Python中copy()与deepcopy()的区别详解
本文深入解析Python中copy()与deepcopy()的核心区别。通过变量引用机制、标签比喻及代码示例,阐明浅拷贝仅复制顶层对象而共享子对象引用,深拷贝则递归创建独立副本。掌握两者差异有助于避免修改嵌套数据时的意外副作用,提升代码安全性。
前言
要准确区分copy()与deepcopy(),必须首先理解Python对数据存储的基本机制。
所谓深复制,是指将原对象完全重新复制一遍,生成一个独立的新个体。因此,修改原始被复制对象时,不会影响已经复制出来的新对象。
浅复制则不会创建一个完全独立的新对象,它只是给原有的数据块添加了一个新的标签。因此,当其中一个标签指向的数据发生变化时,数据块随之改变,另一个标签也会反映出这一变化。
import copy origin = [1, 2, [3, 4]] #origin 里边有三个元素:1, 2,[3, 4] cop1 = copy.copy(origin) cop2 = copy.deepcopy(origin) cop1 == cop2 ------>True cop1 is cop2 ------>False #cop1 和 cop2 看上去相同,但已不再是同一个object origin[2][0] = "hey!" origin ------>[1, 2, ['hey!', 4]] cop1 ------>[1, 2, ['hey!', 4]] cop2 ------>[1, 2, [3, 4]]
观察代码结果可知,cop1(即使用copy生成的副本)随着origin的修改而改变;而cop2(即使用deepcopy生成的副本)保持不变。
Python存储方式
Python存储变量的方式与其他面向对象编程语言(OOP)不同。它并非将值直接赋给变量,而是为变量建立指向具体值的引用(reference)。
在Python中执行a = something时,应理解为给something这个对象贴上了一个名为a的标签。若再次对a赋值,则相当于将a标签从原来的对象上取下,贴到新的对象上,从而建立新的引用关系。这一机制解释了Python中一些看似诡异的现象:
>> a = [1, 2, 3] >>> b = a >>> a = [4, 5, 6] //赋新的值给 a >>> a [4, 5, 6] >>> b [1, 2, 3] # a 的值改变后,b 并没有随着 a 变 >>> a = [1, 2, 3] >>> b = a >>> a[0], a[1], a[2] = 4, 5, 6 //改变原来 list 中的元素 >>> a [4, 5, 6] >>> b [4, 5, 6] # a 的值改变后,b 随着 a 变了
上述两段代码中,变量a的值均发生了变化。区别在于:第一段代码中,a被直接赋予了新值(从[1, 2, 3]变为[4, 5, 6]);而第二段代码中,则是分别修改了列表中的每个元素。
然而,对变量b的影响却截然不同:一种情况下b的值未变,另一种情况下b的值发生了改变。如何利用上述引用理论来解释这种差异呢?
首先将[1, 2, 3]视为一个对象实体。a = [1, 2, 3]相当于给该实体贴上标签a;而b = a则是给同一实体再贴上标签b。

第一种情况:
执行a = [4, 5, 6]时,相当于将标签a从[1, 2, 3]上撕下,重新贴到[4, 5, 6]这个新对象上。
在此过程中,[1, 2, 3]这个对象实体并未消失。标签b自始至终都贴在[1, 2, 3]上,且其指向的引用未发生任何改变,因此b的值自然保持不变。

第二种情况:
执行a[0], a[1], a[2] = 4, 5, 6时,是直接修改了[1, 2, 3]这个对象实体本身,对其内部元素进行了重新赋值。
修改完成后,[1, 2, 3]这个对象本身的内容变成了[4, 5, 6]。在此过程中,标签a和b均未移动,它们依然指向该对象,因此a和b的值都变成了[4, 5, 6]。
理解了这一机制后,我们需要探讨:对一个复杂对象进行浅拷贝(shallow copy)时,具体发生了什么? 参考以下代码:
>>> import copy >>> origin = [1, 2, [3, 4]] #origin 里边有三个元素:1, 2,[3, 4] >>> cop1 = copy.copy(origin) >>> cop2 = copy.deepcopy(origin) >>> cop1 == cop2 True >>> cop1 is cop2 False #cop1 和 cop2 看上去相同,但已不再是同一个object >>> origin[2][0] = "hey!" >>> origin [1, 2, ['hey!', 4]] >>> cop1 [1, 2, ['hey!', 4]] >>> cop2 [1, 2, [3, 4]] #把origin内的子list [3, 4] 改掉了一个元素,观察 cop1 和 cop2
熟悉Docker的用户对“镜像”概念应不陌生,我们可以借用这一概念来类比copy的行为。
概念示意图如下:

对于复杂对象,copy并不会完全复制其所有子对象。所谓复杂对象的子对象,例如序列中的嵌套序列、字典中的嵌套序列等。对于这类子对象,Python会将其视为公共镜像存储,所有对该子对象的复制操作都被视为引用。因此,当其中一个引用修改了镜像内容后,其他引用使用该镜像时,看到的已是被修改后的内容。
具体来看这里的origin[2],即列表[3, 4]。根据浅拷贝的定义,cop1[2]指向的是与origin[2]相同的列表对象[3, 4]。因此,如果我们修改了这个列表,将导致origin和cop1同时发生变化。这就是为什么在执行origin[2][0] = “hey!”后,cop1也随之变为[1, 2, ['hey!', 4]]的原因。
而deepcopy的概念示意图如下:

执行deepcopy时,会将复杂对象的每一层都复制为一个独立的个体。
此时,origin[2]和cop2[2]虽然值均为[3, 4],但它们已不再是同一个列表对象,即实现了我们通常意义上的完全复制。
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









