深度解析python中的赋值与拷贝
1. “与 众 不 同” 的 Python 赋 值
问题引入
【code1】以下赋值操作的输出结果是我们都可以接受的:
a = 2 b = a b = 1 print(a)
Out[i]:
2
在对数值变量的赋值中,b = 1
并没改变变量a
的值。
【code2】以下对列表赋值的输出让我们意外
a = [1,2,3] b = a b[0] = 9 print(a)
Out[i]:
[9, 2, 3]
对列表b
的第0号元素赋值时,却同时改变了列表a
。
【code3】其实意外的结果也可以发生在对同一个变量的赋值过程中:
a = 1 print(id(a)) a = a + a print(id(a))
Out[i]:
140710983841568 140710983841600
为了一探究竟,我决定替奥特曼深入调查一下【code1】、【code2】中小怪兽们的身份:
- 在【code1】中,输出a,b的id:
print(id(a),id(b))
Out[i]:
140710983841600 140710983841568
可以看到:a、b不再是相同id,则它们具有不同的内存地址。- 在【code2】中,输出a,b的id:
print(id(a),id(b))
Out[i]:
2613318275968 2613318275968
可以看到:a、b仍然是同一id,可知a、b仍引用到同一内存地址。
问题刨析
Python是彻底的面向对象编程语言,从Python对象的角度来理解
研究上面的输出后,学过C++的"童鞋"第一次遇到这种“架势”可能会感到很吃惊和疑惑。
但这时应当注意,虽然也支持面向过程编程的风格,但python是一种彻底的面向对象语言,数值可以看作实例对象,比如数值1
就是int
类的实例。每个对象都有自己的一块内存地址。对象分为不可变对象和可变对象。
从本质上看Python中的直接赋值是对象的引用,传递的是对象间的地址而不是值的拷贝。
- 先看比较容易理解的【code3】:
a = 1 # a的引用为对象1的地址 a = a + a # 先执行语句a+a得到一个新的对象2,再执行赋值。 # 赋值时,2是一个新的不可变对象,以前的对象(也就是1)是不可变对象,不会被改变也不能被覆盖 # 因此a最终引用到不可变对象2的内存地址
这个过程可以绘制一个图来描述:
这就是【code3】中,变量a的id
发生改变的原因。
- 同理再看【code1】
在【code1】中,数值对象是不可变对象,在被创造之后,它的状态就不可以被改变。但尽管对象本身不可变,但变量的对象引用是可变的
a = 2 # 变量a引用到不可变对象2的地址 b = a # b通过a的引用到2 b = 1 # b引用到另外一个对象,即数值常量1在这条语句时有系统分配的地址
这个过程可以绘制一个图来描述:
b的引用改变了,指向了另外一个不可变对象,也就是1的地址。最终a、b具有不同的内存地址。
- 最后看【code2】
在【code2】中,列表是可变对象,对象的内容是可变的。在Python中这种可变对象一般是复合数据结构,如list、dict、tuple、set等等,其内容是可变的原因在于它们都是复合数据结构,每一个内容元素本身也是一个对象。
a = [1,2,3] # a 获得了单条语句执行时系统分配的列表[1,2,3]的首地址(python是脚本语言) b = a # b引用到与a同一列表的首地址! b[0] = 9 # 使变量b引用的列表(也是变量a引用的列表)的第一个元素引用到不可变对象9
也就是这种原因,【code2】中可变对象a
在赋值时geib
时,b
与a
引用到同一对象,而赋值语句b[0] = 9
仅仅是对这个数组对象的子对象b[0]
的引用进行了改变,并没有改变变量b
本身的引用关系!
不信我们可以做如下验证:
a = [1,2,3] b = a print("id(a)=",id(a),"id(b)=",id(b)) print("id(a[0])=",id(a[0]),"id(b[0])=",id(b[0])) b[0] = 9 print("\nid(a)=",id(a),"id(b)=",id(b)) print("id(a[0])=",id(a[0]),"id(b[0])=",id(b[0]))
Out[i]:
id(a)= 2613322289408 id(b)= 2613322289408 id(a[0])= 140710983841568 id(b[0])= 140710983841568 id(a)= 2613322289408 id(b)= 2613322289408 id(a[0])= 140710983841824 id(b[0])= 140710983841824
可以看到,a与b的id始终没变,但它们随引用的这个数组的第一个元素的引用却同时改变了,完全符合我以上所述。
其它类似疑惑
在理解了以上问题后就会理解一些类似的操作在Python简直就是扯淡,比如有人做如下谜一样的操作:
a = [1,2,3] a[0]=a a
Out[i]:
[[...], 2, 3]
这结果他自己也看不懂了,就是觉得是谜一样的结果。在这个过程中,很显然没有理解到python中的赋值操作,使得列表a的头一个元素无线迭代指向原列表a自身,这样的代码几乎是没有意义的。
2. 浅拷贝与深拷贝
Python 的赋值语句不复制对象,而是创建目标和对象的绑定关系。对于自身可变,或包含可变项的集合,有时要生成副本用于改变操作,而不必改变原始对象。
2.1 浅拷贝(浅层赋值 shallow copy)
所谓浅拷贝它仅仅拷贝父对象,不会拷贝对象的内部的子对象。即浅层复制 构造一个新的复合对象,然后(在尽可能的范围内)将原始对象中找到的对象的 引用 插入其中。
2.2 深拷贝(深度复制 deep copy)
copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。即深层复制 构造一个新的复合对象,然后,递归地将在原始对象里找到的对象的 副本 插入其中。
深度复制操作通常存在两个问题, 而浅层复制操作并不存在这些问题:
- 递归对象 (直接或间接包含对自身引用的复合对象) 可能会导致递归循环。
- 由于深层复制会复制所有内容,因此可能会过多复制(例如本应该在副本之间共享的数据)。