一、赋值、引用
在python中赋值语句总是建立对象的引用值,而不是复制对象。因此,python变量更像是指针,而不是数据存储区域
这点和大多数语音类似吧,比如C++、Java等
1、先看个例子:
values=[0,1,2] values[1]=values print(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 去。过程如图所示:
执行:values[1]=values的时候,python做的事情则是把values这个标签所引用的列表对象的第二个元素指向values所引用的列表对象本身。执行完毕后,values
标签还是指向原来那个对象,只不过那个对象的结构发生了变化,从之前的列表[0,1,2]变成了[0,?,2],而这个?则是指向那个对象本身的
一个引用。如图所示:
要达到你所需要的效果,即得到 [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]。过程如图所示:
往更深处说,values[:] 复制操作是所谓的「浅复制」(shallow copy),当列表对象有嵌套的时候也会产生出乎意料的错误,比如
a=[0,[1,2],3] b=a[:] a[0]=8 a[1][1]=9 print(a) # [8, [1, 9], 3] print(b) # [0, [1, 9], 3]
b 的第二个元素也被改变了。想想是为什么?不明白的话看下图
正确的复制嵌套元素的方法是进行「深复制」(deep copy),方法是
import copy a = [0, [1, 2], 3] b = copy.deepcopy(a) a[0] = 8 a[1][1] = 9
2、引用vs拷贝
(1)没有限制条件的分片表达式(L[:])能够复制序列,但此法只能浅层复制。
(2)字典 copy 方法,D.copy() 能够复制字典,但此法只能浅层复制
(3)有些内置函数,例如 list,能够生成拷贝 list(L)
(4)copy 标准库模块能够生成完整拷贝:deepcopy 本质上是递归 copy
(5)对于不可变对象和可变对象来说,浅复制都是复制的引用,只是因为复制不变对象和复制不变对象的引用是等效的(因为对象不可变,当改变时会新建对象重新赋值)。所以看起来浅复制只复制不可变对象(整数,实数,字符串等),对于可变对象,浅复制其实是创建了一个对于该对象的引用,也就是说只是给同一个对象贴上了另一个标签而已。
3、增强赋值以及共享引用
x = x + y,x 出现两次,必须执行两次,性能不好,合并必须新建对象 x,然后复制两个列表合并
属于复制/拷贝
x += y,x 只出现一次,也只会计算一次,性能好,不生成新对象,只在内存块末尾增加元素。
当 x、y 为list时, += 会自动调用 extend 方法进行合并运算,in-place change。
属于共享引用
二、深拷贝deepcopy与浅拷贝copy
python中的对象之间赋值时是按引用传送的,如果需要拷贝对象,需要使用标准库中的copy模块
1、copy.copy 浅拷贝,只拷贝父对象,不会拷贝对象的内部的子对象。(子对象(数组)修改,也会修改)
2、copy.deepcopy 深拷贝,拷贝对象及其子对象(原始对象)
import copy a=[1,2,[3,4],{'a':1}] # 原始对象 b=a # 赋值,传对象的引用 c=copy.copy(a) # 对象拷贝,浅拷贝 d=copy.deepcopy(a) # 对象拷贝,深拷贝 e=a[:] # 能复制序列,浅拷贝 a.append('add1') # 修改对象a a[2].append('add2') # 修改对象a中的[3,4]数组对象 a[3]='666' print('a:',a) print('b:',b) print('c:',c) print('d:',d) print('e:',e)
"""
执行结果:
a: [1, 2, [3, 4, 'add2'], '666', 'add1']
b: [1, 2, [3, 4, 'add2'], '666', 'add1']
c: [1, 2, [3, 4, 'add2'], {'a': 1}]
d: [1, 2, [3, 4], {'a': 1}]
e: [1, 2, [3, 4, 'add2'], {'a': 1}]
解释:copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象。子对象(数组)修改,也会修改
copy.deepcopy 深拷贝 拷贝对象及其子对象(原始对象)
"""
三、深入理解python变量作用域及其陷阱
1、可变对象&不可变对象
在Python中,对象分为两种:可变对象和不可变对象,不可变对象包括int,float,long,str,tuple等,可变对象包括list,set,dict等。需要注意的是:这里说的不可变指的是值的不可变。对于不可变类型的变量,如果要更改变量,则会创建一个新值,把变量绑定到新值上,而旧值如果没有被引用就等待垃圾回收。另外,不可变的类型可以计算hash值,作为字典的key。可变类型数据对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请(+/-)即可,也就是它的内存地址会保持不变,但区域会变长或者变短。
a='hello' print(id(a)) # 1991608735200 a='python' print(id(a)) # 1991608735368 # 重新赋值之后,变量a的内存地址已经变了 # 'hello'是str类型,不可变,所以赋值操作知识重新创建了str 'python'对象,然后将变量a指向了它 l1=[1,2,3] print(id(l1)) # 2262493958280 l1.append(4) print(id(l1)) # 2262493958280 # list重新赋值之后,变量l1的内存地址并未改变 # [1, 2, 3]是可变的,append操作只是改变了其value,变量l1指向没有变
2、函数值传递
def func_int(a): a+=4 def func_list(l1): l1[0]=4 t=0 func_int(t) print(t) # 0 t_list=[1,2,3] func_list(t_list) print(t_list) # [4, 2, 3]
对于上面的输出,不少Python初学者都比较疑惑:第一个例子看起来像是传值,而第二个例子确实传引用。其实,解释这个问题也非常容易,主要是因为可变对象和不可变对象的原因:对于可变对象,对象的操作不会重建对象,而对于不可变对象,每一次操作就重建新的对象。
在函数参数传递的时候,Python其实就是把参数里传入的变量对应的对象的引用依次赋值给对应的函数内部变量。参照上面的例子来说明更容易理解,func_int中的局部变量"a"其实是全部变量"t"所指向对象的另一个引用,由于整数对象是不可变的,所以当func_int对变量"a"进行修改的时候,实际上是将局部变量"a"指向到了整数对象"1"。所以很明显,func_list修改的是一个可变的对象,局部变量"a"和全局变量"t_list"指向的还是同一个对象。
3、陷阱:使用可变的默认参数
我多次见到过如下的代码: 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在函数调用的每一次都被反复引用。这可能有一些意想不到的后果。
参考:http://www.cnblogs.com/jiangzhaowei/p/5740913.html