betway必威-betway必威官方网站
做最好的网站

深入理解,引用传参

导读:

1.变量和对象

2.可变对象与不可变对象

3.引用传参

 

在C/C 中,传值和传引用是函数参数传递的两种方式。由于思维定式,从C/C 转过来的Python初学者也经常会感到疑惑:在Python中,函数参数传递是传值,还是传引用呢?
看下面两段代码:

def foo(arg):
arg = 5
print(arg)
x = 1
foo(x) # 输出5
print(x) # 输出1

def foo(arg):
arg.append(3)
x = [1, 2]
print(x) # 输出[1, 2]
foo(x)
print(x) # 输出[1, 2, 3]

看完第一段代码,会有人说这是值传递,因为函数并没有改变x的值;看完第二段代码,又会有人说这是传引用,因为函数改变了x的内容。
那么,Python中的函数到底是传值还是传引用呢?

a = a 与 a = a a 的区别

在 python 中赋值语句总是建立对象的引用值,而不是复制对象。因此,python 变量更像是指针,而不是数据存储区域,

一、变量和对象

我们需要先知道Python中的“变量”与C/C 中“变量”是不同的。
在C/C 中,当你初始化一个变量时,就是声明一块存储空间并写入值。相当于把一个值放入一个盒子里:
int a = 1;
图片 1

现在”a”盒子里放了一个整数1,当给变量a赋另外一个值时会替换盒子a里面的内容:
a = 2;

图片 2

当你把变量a赋给另外一个变量时,会拷贝a盒子中的值并放入一个新的“盒子”里:
int b = a;

图片 3

在Python中,一个变量可以说是内存中的一个对象的“标签”或“引用”:
a = 1

图片 4

现在变量a指向了内存中的一个int型的对象(a相当于对象的标签)。如果给a重新赋值,那么标签a将会移动并指向另一个对象:
a = 2

图片 5

原来的值为1的int型对象仍然存在,但我们不能再通过a这个标识符去访问它了(当一个对象没有任何标签或引用指向它时,它就会被自动释放)。如果我们把变量a赋给另一个变量,我们只是给当前内存中对象增加一个“标签”而已:
b = a

图片 6

综上所述,在Python中变量只是一个标签一个标识符,它指向内存中的对象。故变量并没有类型,类型是属于对象的,这也是Python中的变量可以被任何类型赋值的原因。

在python中,值是靠引用来传递来的。
我们可以用id()来判断两个变量是否为同一个值的引用。 我们可以将id值理解为那块内存的地址标示。

图片 7图片 8

>>> a = 1
>>> b = a
>>> id(a) 
13033816
>>> id(b) # 注意两个变量的id值相同
13033816
>>> a = 2
>>> id(a) # 注意a的id值已经变了
13033792
>>> id(b) # b的id值依旧
13033816

>>> a = [1, 2]
>>> b = a
>>> id(a)
139935018544808
>>> id(b)
139935018544808
>>> a.append(3)
>>> a
[1, 2, 3]
>>> id(a)
139935018544808
>>> id(b) # 注意a与b始终指向同一个地址
139935018544808

View Code

 

可变类型a = a a 的示例

图片 9

二、可变对象与不可变对象

在Python的基本数据类型中,我们知道numbers、strings和tuples是不可更改的对象,而list、dict、set是可以修改的对象。那么可变与不可变有什么区别呢?看下面示例:

a = 1 # a指向内存中一个int型对象
a = 2 # 重新赋值

当将a重新赋值时,因为原来值为1的对象是不能改变的,所以a会指向一个新的int对象,其值为2。(如上面的图示)

lst = [1, 2] # lst指向内存中一个list类型的对象
lst[0] = 2 # 重新赋值lst中第一个元素

因为list类型是可以改变的,所以第一个元素变更为2。更确切的说,lst的第一个元素是int型,重新赋值时一个新的int对象被指定给第一个元素,但是对于lst来说,它所指的列表型对象没有变,只是列表的内容(其中一个元素)改变了。
好了,到这里我们就很容易解释开头的两段代码了:

def foo(arg):
arg = 5
print(arg)
x = 1
foo(x) # 输出5
print(x) # 输出1

 

这点和大多数 OO 语言类似吧,比如 C 、java 等 ~

上面这段代码把x作为参数传递给函数,这时x和arg都指向内存中值为1的对象。然后在函数中arg

5时,因为int对象不可改变,于是创建一个新的int对象(值为5)并且令arg指向它。而x仍然指向原来的值为1的int对象,所以函数没有改变x变量。

def foo(arg):
arg.append(3)
x = [1, 2]
print(x) # 输出[1, 2]
foo(x)
print(x) # 输出[1, 2, 3]

这段代码同样把x传递给函数foo,那么x和arg都会指向同一个list类型的对象。因为list对象是可以改变的,函数中使用append在其末尾添加了一个元素,list对象的内容发生了改变,但是x和arg仍然是指向这一个list对象,所以变量x的内容发生了改变。

In [58]: a = [11,22]


In [59]: id(a)
Out[59]: 140702917607688


In [60]: a = a   a


In [61]: a 
Out[61]: [11, 22, 11, 22]


In [62]: id(a)
Out[62]: 140703006930440


In [63]: # 注意id的结果不同

1、先来看个问题吧:

在Python中,令values=[0,1,2];values[1]=values,为何结果是[0,[...],2]?

>>> values = [0, 1, 2]
>>> values[1] = values
>>> values
[0, [...], 2]

我预想应当是 

[0, [0, 1, 2], 2]

但结果却为何要赋值无限次?

 

可以说 Python 没有赋值,只有引用。你这样相当于创建了一个引用自身的结构,所以导致了无限循环。为了理解这个问题,有个基本概念需要搞清楚。

Python 没有「变量」,我们平时所说的变量其实只是「标签」,是引用。

执行 

values = [0, 1, 2]

的时候,Python 做的事情是首先创建一个列表对象 [0, 1, 2],然后给它贴上名为 values 的标签。如果随后又执行

values = [3, 4, 5]

的话,Python 做的事情是创建另一个列表对象 [3, 4, 5],然后把刚才那张名为 values 的标签从前面的 [0, 1, 2] 对象上撕下来,重新贴到 [3, 4, 5] 这个对象上。

至始至终,并没有一个叫做 values 的列表对象容器存在,Python 也没有把任何对象的值复制进 values 去。过程如图所示:
图片 10

执行

values[1] = values

的时候,Python 做的事情则是把 values 这个标签所引用的列表对象的第二个元素指向 values 所引用的列表对象本身。执行完毕后,values 标签还是指向原来那个对象,只不过那个对象的结构发生了变化,从之前的列表 [0, 1, 2] 变成了 [0, ?, 2],而这个 ? 则是指向那个对象本身的一个引用。如图所示:
图片 11
要达到你所需要的效果,即得到 [0, [0, 1, 2], 2] 这个对象,你不能直接将 values[1] 指向 values 引用的对象本身,而是需要吧 [0, 1, 2] 这个对象「复制」一遍,得到一个新对象,再将 values[1] 指向这个复制后的对象。Python 里面复制对象的操作因对象类型而异,复制列表 values 的操作是

values[:] #生成对象的拷贝或者是复制序列,不再是引用和共享变量,但此法只能顶层复制

所以你需要执行

values[1] = values[:]

Python 做的事情是,先 dereference 得到 values 所指向的对象 [0, 1, 2],然后执行 [0, 1, 2][:] 复制操作得到一个新的对象,内容也是 [0, 1, 2],然后将 values 所指向的列表对象的第二个元素指向这个复制二来的列表对象,最终 values 指向的对象是 [0, [0, 1, 2], 2]。过程如图所示:
图片 12

往更深处说,values[:] 复制操作是所谓的「浅复制」(shallow copy),当列表对象有嵌套的时候也会产生出乎意料的错误,比如

a = [0, [1, 2], 3]
b = a[:]
a[0] = 8
a[1][1] = 9

问:此时 a 和 b 分别是多少?

正确答案是 a 为 [8, [1, 9], 3],b 为 [0, [1, 9], 3]。发现没?b 的第二个元素也被改变了。想想是为什么?不明白的话看下图
图片 13

正确的复制嵌套元素的方法是进行「深复制」(deep copy),方法是

 

import copy

a = [0, [1, 2], 3]
b = copy.deepcopy(a)
a[0] = 8
a[1][1] = 9

图片 14

三、引用传参

可变类型与不可变类型的变量分别作为函数参数时,会有什么不同吗?
Python有没有类似C语言中的指针传参呢?

>>> def selfAdd(a):
... """自增"""
... a  = a
...
>>> a_int = 1
>>> a_int
1
>>> selfAdd(a_int)
>>> a_int
1
>>> a_list = [1, 2]
>>> a_list
[1, 2]
>>> selfAdd(a_list)
>>> a_list
[1, 2, 1, 2]

Python中函数参数是引用传递(注意不是值传递)。对于不可变类型,因变量不能修改,所以运算不会影响到变量自身;而对于可变类型来说,函数体中的运算有可能会更改传入的参数变量。

想一想为什么

>>> def selfAdd(a):
... """自增"""
... a = a   a # 我们更改了函数体的这句话
...
>>> a_int = 1
>>> a_int
1
>>> selfAdd(a_int)
>>> a_int
1
>>> a_list = [1, 2]
>>> a_list
[1, 2]
>>> selfAdd(a_list)
>>> a_list
[1, 2] # 想一想为什么没有变呢?

总结:
x = x是直接对x指向的空间进行修改,而不是让b指向一个新的。
x = x x先把=号右边的结果计算出来,然后让x指向这个新的地方,不管原来b指向谁.

分析以上的代码:
  第一步:计算赋值运算符右边的代码 [11,22] [11,22] = [11,22,11,22]
  第二步:将计算的新结果开辟了新的内存保存
  第三步:让a指向了新的内存

2、引用 VS 拷贝:

(1)没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制。

(2)字典 copy 方法,D.copy() 能够复制字典,但此法只能浅层复制

(3)有些内置函数,例如 list,能够生成拷贝 list(L)

(4)copy 标准库模块能够生成完整拷贝:deepcopy 本质上是递归 copy

(5)对于不可变对象和可变对象来说,浅复制都是复制的引用,只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等),对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。

L = [1, 2, 3]
D = {'a':1, 'b':2}
A = L[:]
B = D.copy()
print "L, D"
print  L, D
print "A, B"
print A, B
print "--------------------"
A[1] = 'NI'
B['c'] = 'spam'
print "L, D"
print  L, D
print "A, B"
print A, B


L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 2, 3] {'a': 1, 'b': 2}
--------------------
L, D
[1, 2, 3] {'a': 1, 'b': 2}
A, B
[1, 'NI', 3] {'a': 1, 'c': 'spam', 'b': 2}

 四、一切皆对象

Python使用对象模型来储存数据,任何类型的值都是一个对象。所有的python对象都有3个特征:身份id、类型type和值value。
身份:每一个对象都有自己的唯一的标识,可以使用内建函数id()来得到它。这个值可以被认为是该对象的内存地址。
类型:对象的类型决定了该对象可以保存的什么类型的值,可以进行什么操作,以及遵循什么样的规则。type()函数来查看python 对象的类型。
:对象表示的数据项。

>>> a = 1
>>> id(a)
140068196051520
>>> b = 2
>>> id(b)
140068196051552
>>> c = a
>>> id(c)
140068196051520
>>> c is a
True
>>> c is not b
True

运算符 is 、 is not 就是通过id()的返回值(即身份)来判定的,也就是看它们是不是同一个对象的“标签”。

 

本文来源于我看到的一篇文档,具体来源不可考,我觉得对于引用讲的还是比较清楚的。引用以及可变对象不可变对象,在Python中时比较重要的,因为在接下来的学习中,都会有意无意地用到。

 

可变类型的a = a示例

3、增强赋值以及共享引用:

x = x y,x 出现两次,必须执行两次,性能不好,合并必须新建对象 x,然后复制两个列表合并

属于复制/拷贝

x = y,x 只出现一次,也只会计算一次,性能好,不生成新对象,只在内存块末尾增加元素。

当 x、y 为list时, = 会自动调用 extend 方法进行合并运算,in-place change。

属于共享引用

L = [1, 2]
M = L
L = L   [3, 4]
print L, M
print "-------------------"
L = [1, 2]
M = L
L  = [3, 4]
print L, M


[1, 2, 3, 4] [1, 2]
-------------------
[1, 2, 3, 4] [1, 2, 3, 4]
In [63]: a = [11,22]

In [64]: id(a)
Out[64]: 140702994655880

In [65]: a  = a

In [66]: id(a)
Out[66]: 140702994655880

In [67]: a
Out[67]: [11, 22, 11, 22] #注意id的值没变

4、python 从 2k 到 3k,语句变函数引发的变量作用域问题  

先看段代码:

def test():
    a = False
    exec ("a = True")
    print ("a = ", a)
test()

b = False
exec ("b = True")
print ("b = ", b)

在 python 2k 和 3k 下 你会发现他们的结果不一样:

2K:
a =  True
b =  True

3K:
a =  False
b =  True

这是为什么呢?

因为 3k 中 exec 由语句变成函数了,而在函数中变量默认都是局部的,也就是说

你所见到的两个 a,是两个不同的变量,分别处于不同的命名空间中,而不会冲突。

具体参考 《learning python》P331-P332

知道原因了,我们可以这么改改:

def test():
    a = False
    ldict = locals()
    exec("a=True",globals(),ldict)
    a = ldict['a']
    print(a)

test()

b = False
exec("b = True", globals())
print("b = ", b)

这个问题在  stackoverflow 上已经有人问了,而且 python 官方也有人报了 bug。。。

具体链接在下面:

这是一个典型的 python 2k 移植到 3k 不兼容的案例,类似的还有很多,也算是移植的坑吧~

具体的 2k 与 3k 有哪些差异可以看这里:

使用 2to3 将代码移植到 Python 3

 

5、深入理解 python 变量作用域及其陷阱

分析以上的代码:
  a = a  是在原来a的指向的内存里修改值 a的指向并没有修改

5.1 可变对象 & 不可变对象

在Python中,对象分为两种:可变对象和不可变对象,不可变对象包括int,float,long,str,tuple等,可变对象包括list,set,dict等。需要注意的是:这里说的不可变指的是值的不可变。对于不可变类型的变量,如果要更改变量,则会创建一个新值,把变量绑定到新值上,而旧值如果没有被引用就等待垃圾回收。另外,不可变的类型可以计算hash值,作为字典的key。可变类型数据对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请( /-)即可,也就是它的内存地址会保持不变,但区域会变长或者变短。

>>> a = 'xianglong.me'
>>> id(a)
140443303134352
>>> a = '1saying.com'
>>> id(a)
140443303131776
# 重新赋值之后,变量a的内存地址已经变了
# 'xianglong.me'是str类型,不可变,所以赋值操作知识重新创建了str '1saying.com'对象,然后将变量a指向了它
 
>>> a_list = [1, 2, 3]
>>> id(a_list)
140443302951680
>>> a_list.append(4)
>>> id(a_list)
140443302951680
# list重新赋值之后,变量a_list的内存地址并未改变
# [1, 2, 3]是可变的,append操作只是改变了其value,变量a_list指向没有变

注意以上是可变类型  下面看一下不可变类型

5.2 函数值传递

def func_int(a):
    a  = 4
 
def func_list(a_list):
    a_list[0] = 4
 
t = 0
func_int(t)
print t
# output: 0
 
t_list = [1, 2, 3]
func_list(t_list)
print t_list
# output: [4, 2, 3]

 

 对于上面的输出,不少Python初学者都比较疑惑:第一个例子看起来像是传值,而第二个例子确实传引用。其实,解释这个问题也非常容易,主要是因为可变对象和不可变对象的原因:对于可变对象,对象的操作不会重建对象,而对于不可变对象,每一次操作就重建新的对象。

    在函数参数传递的时候,Python其实就是把参数里传入的变量对应的对象的引用依次赋值给对应的函数内部变量。参照上面的例子来说明更容易理解,func_int中的局部变量"a"其实是全部变量"t"所指向对象的另一个引用,由于整数对象是不可变的,所以当func_int对变量"a"进行修改的时候,实际上是将局部变量"a"指向到了整数对象"1"。所以很明显,func_list修改的是一个可变的对象,局部变量"a"和全局变量"t_list"指向的还是同一个对象。

In [68]: a = 1

In [69]: id(a)
Out[69]: 10914368

In [70]: a  = a

In [71]: id(a)
Out[71]: 10914400

In [72]: a
Out[72]: 2

====================================================
In [76]: a = 1

In [77]: id(a)
Out[77]: 10914368

In [78]: a = a   a

In [79]: id(a)
Out[79]: 10914400

In [80]: a
Out[80]: 2

5.3 为什么修改全局的dict变量不用global关键字

为什么修改字典d的值不用global关键字先声明呢?

s = 'foo'
d = {'a':1}
def f():
    s = 'bar'
    d['b'] = 2
f()
print s  # foo
print d  # {'a': 1, 'b': 2}

这是因为,在s = 'bar'这句中,它是“有歧义的“,因为它既可以是表示引用全局变量s,也可以是创建一个新的局部变量,所以在python中,默认它的行为是创建局部变量,除非显式声明global,global定义的本地变量会变成其对应全局变量的一个别名,即是同一个变量。

在d['b']=2这句中,它是“明确的”,因为如果把d当作是局部变量的话,它会报KeyError,所以它只能是引用全局的d,故不需要多此一举显式声明global。

上面这两句赋值语句其实是不同的行为,一个是rebinding(不可变对象), 一个是mutation(可变对象).

但是如果是下面这样:

d = {'a':1}
def f():
    d = {}
    d['b'] = 2
f()
print d  # {'a': 1}

 

在d = {}这句,它是”有歧义的“了,所以它是创建了局部变量d,而不是引用全局变量d,所以d['b']=2也是操作的局部变量。

推而远之,这一切现象的本质就是”它是否是明确的“。

仔细想想,就会发现不止dict不需要global,所有”明确的“东西都不需要global。因为int类型str类型之类的不可变对象,每一次操作就重建新的对象,他们只有一种修改方法,即x = y, 恰好这种修改方法同时也是创建变量的方法,所以产生了歧义,不知道是要修改还是创建。而dict/list/对象等可变对象,操作不会重建对象,可以通过dict['x']=y或list.append()之类的来修改,跟创建变量不冲突,不产生歧义,所以都不用显式global。

  

5.4 可变对象 list 的 = 和 append/extend 差别在哪?

接上面 5.3 的理论,下面咱们再看一例常见的错误:

# coding=utf-8
# 测试utf-8编码
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

list_a = []
def a():
    list_a = [1]      ## 语句1
a()
print list_a    # []

print "======================"

list_b = []
def b():
    list_b.append(1)    ## 语句2
b()
print list_b    # [1]

大家可以看到为什么 语句1 不能改变 list_a 的值,而 语句2 却可以?他们的差别在哪呢?

因为 = 创建了局部变量,而 .append() 或者 .extend() 重用了全局变量。

总结:

5.5 陷阱:使用可变的默认参数

我多次见到过如下的代码:

def foo(a, b, c=[]):
# append to c
# do some more stuff

永远不要使用可变的默认参数,可以使用如下的代码代替:

def foo(a, b, c=None):
    if c is None:
        c = []
    # append to c
    # do some more stuff

‍‍与其解释这个问题是什么,不如展示下使用可变默认参数的影响:‍‍

In[2]: def foo(a, b, c=[]):
...        c.append(a)
...        c.append(b)
...        print(c)
...
In[3]: foo(1, 1)
[1, 1]
In[4]: foo(1, 1)
[1, 1, 1, 1]
In[5]: foo(1, 1)
[1, 1, 1, 1, 1, 1]

同一个变量c在函数调用的每一次都被反复引用。这可能有一些意想不到的后果。

REF:

  如果a是一个可变类型,那么a = a 是在a指向的内存中直接修改,a = a a 是指向了一个新的内存
    如果a是一个不可变类型,那么a = a 和a = a a 的效果一样即:a指向了一个新的内存

 

本文由betway必威发布于编程开发,转载请注明出处:深入理解,引用传参

TAG标签: betway必威
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。