Python 中的浅拷贝和深拷贝
在讨论浅拷贝和深拷贝之前,首先要了解 python 中常见的两个对象的比较场景。
==和 is
==
操作符进行的是对象的值判断,比较两个对象的值是否相等。
is
操作符进行的是对象的身份标识的判断,比较两个对象的内存地址是否相等。
None 在 Python 中比较特殊,在 Python 里是个单例对象,一个变量如果是 None,它一定和 None 指向同一个内存地址。None 是 python 中的一个特殊的常量,表示一个空的对象,空值是 python 中的一个特殊值。数据为空并不代表是空对象,例如[],''等都不是 None。None 和任何对象比较返回值都是 False,除了自己。
举个例子:
首先 Python 会为 1 这个值开辟一块内存,然后变量 a 和 b 同时指向这块内存地址,即 a 和 b 都是指向 1 这个变量,因此 a 和 b 的值相等,id 也相等,a == b 和 a is b 都返回 True。
不过,需要注意,对于整型数字来说,以上 a is b 为 True 的结论,只适用于 [-5 , 257 )范围内的数字。
出于对性能优化的考虑,Python 内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。每次创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的值的引用,而不是重新频繁地开辟一块新的内存空间。
上述例子中的 369,Python 则会为两个 369 开辟两块内存区域,因此 a 和 b 的 ID 不一样,a is b 就会返回 False 了。
浅拷贝 shallow copy
浅拷贝通常的实现方法是使用数据类型本身的构造器,此处所说通常情况,是因为后面存在着特殊情况(元组的构造器),比如:
当然,Python 中也提供了相对应的函数 copy.copy(),适用于任何数据类型,用来进行浅拷贝:
前面提到了元组的特殊情况,使用 tuple() 或者切片操作符':'不会创建一份浅拷贝,相反,它会返回一个指向相同元组的引用。实际上,使用 copy.copy()以及 copy.deepcopy()得到的,也是一个指向相同元组的引用。
浅拷贝,是指重新分配一块内存,创建一个新的对象,里面的元素是原对象中子对象的引用。既然拷贝得到的新的对象的元素是对原对象中子对象的引用,那么有几个点需要注意一下。看一下下面的例子。
这个例子中,我们首先初始化了一个列表 l1,里面的元素是一个列表和一个元组;然后对 l1 执行浅拷贝,赋予 l2。因为浅拷贝里的元素是对原对象元素的引用,因此 l2 中的元素和 l1 指向同一个列表和元组对象。
紧接着,l1.append(100),表示对 l1 的列表新增元素 100。这个操作不会对 l2 产生任何影响,因为 l2 和 l1 作为整体是两个不同的对象,并不共享内存地址。操作过后 l2 不变,l1 会发生改变。
再来看,l1[0].append(3),这里表示对 l1 中的第一个列表新增元素 3。因为 l2 是 l1 的浅拷贝,l2 中的第一个元素和 l1 中的第一个元素,共同指向同一个列表,因此 l2 中的第一个列表也会相对应的新增元素 3。操作后 l1 和 l2 都会改变。
最后是 l1[1] += (50, 60),因为元组是不可变的,这里表示对 l1 中的第二个元组拼接,然后重新创建了一个新元组作为 l1 中的第二个元素,而 l2 中没有引用新元组,因此 l2 并不受影响。操作后 l2 不变,l1 发生改变。
上面的例子,对于可变对象的引用,尤其是列表独享,容易带来一些副作用,要避免这种情况,可以使用深拷贝。
深拷贝 deep copy
Python 中以 copy.deepcopy() 来实现对象的深度拷贝。
可以看到,无论 l1 如何变化,l2 都不变。因为此时的 l1 和 l2 完全独立,没有任何联系。
深度拷贝也不是完美的,往往也会带来一系列问题。如果被拷贝对象中存在指向自身的引用,那么程序很容易陷入无限循环:
列表 x 中有指向自身的引用,因此 x 是一个无限嵌套的列表。但是我们发现深度拷贝 x 到 y 后,程序并没有出现 stack overflow 的现象。这是为什么呢?
这是因为深度拷贝函数 deepcopy 中会维护一个字典,记录已经拷贝的对象与其 ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,我们来看相对应的源码就能明白:
如果执行如下操作:
出现上面错误的原因很明显,在执行 == 操作时,因为 x 中存储了自身的引用,会无限的递归与 y 比较,从而造成 RecursionError 异常,因为最大递归深度有一定的限制。
总结
比较操作符'=='表示比较对象间的值是否相等,而'is'表示比较对象的标识是否相等,即它们是否指向同一个内存地址。
浅拷贝中的元素,是原对象中子对象的引用,因此,如果原对象中的元素是可变的,改变其也会影响拷贝后的对象,存在一定的副作用。
深度拷贝则会递归地拷贝原对象中的每一个子对象,因此拷贝后的对象和原对象互不相关。另外,深度拷贝中会维护一个字典,记录已经拷贝的对象及其 ID,来提高效率并防止无限递归的发生。
最后供上一张图方便理解:
参考资料
版权声明: 本文为 InfoQ 作者【王坤祥】的原创文章。
原文链接:【http://xie.infoq.cn/article/6af34fee334b3595caaf15cfa】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论