在 Python 中涉及到对象拷贝主要有两个问题:
- 深拷贝和浅拷贝问题
- 自定义对象拷贝过程
1. 深浅拷贝
深拷贝和浅拷贝的主要区别在于它们如何处理对象中的可变子对象。对于不可变类型不涉及到深浅拷贝问题。在 Python 中,只有字典、集合、列表属于可变类型。
1.1 浅拷贝
创建一个新的对象,但只复制原对象的引用。这意味着原对象中的可变子对象(如列表、字典、集合等)不会被复制,而是共享相同的引用。修改任何子对象的内容都会影响到原对象和浅拷贝对象。
import copy def ids(elemets): print(id(elemets), end=' ') for element in elemets: print(id(element), end=' ') print() def test(): my_list1 = [[10, 20], {30, 40}, {'k1': 50, 'k2': 60}] my_list2 = copy.copy(my_list1) # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}] # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}] print(my_list1) print(my_list2) # 2271041540416 2271041542272 2271043720064 2271044354496 # 2271041874496 2271041542272 2271043720064 2271044354496 ids(my_list1) ids(my_list2) # 问题:任何一个列表发生改变,其他的列表也会发生改变 if __name__ == '__main__': test()
程序的输出结果:
从输出结果来看,当我们使用 copy 函数或者切片对以一个包含可变类型的列表进行拷贝时,仅仅拷贝了子元素的引用,所以,当通过任意一个列表修改元素时,都会导致其他的元素发生变化。
1.2 深拷贝
创建一个新的对象,并递归地复制原对象及其所有子对象。这意味着修改任何子对象的内容都不会影响原对象或深拷贝对象。对于嵌套数据结构,深拷贝确保每个层级的子对象都是独立的。
使用 copy 模块的 deepcopy 可以实现对象的深拷贝。
import copy def ids(elemets): print(id(elemets), end=' ') for element in elemets: print(id(element), end=' ') print() def test(): my_list1 = [[10, 20], {30, 40}, {'k1': 50, 'k2': 60}] my_list2 = copy.deepcopy(my_list1) # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}] # [[10, 20], {40, 30}, {'k1': 50, 'k2': 60}] print(my_list1) print(my_list2) # 21815361212736 1815361214592 1815363392384 1815364026816 # 1815361546816 1815364626432 1815364488992 1815364632000 ids(my_list1) ids(my_list2) if __name__ == '__main__': test()
程序的执行结果:
对列表进行深拷贝之后,会对列表中的可变类型子元素进行递归拷贝。需要注意的是,对于不可变类型子元素,不用刻意区分深浅拷贝,因为即使是浅拷贝,一个列表修改了不可变类型子元素,也不会影响到另外一个列表。
2. 拷贝协议
Python 通过拷贝协议支持自定义对象的拷贝过程,只需要在类内增加浅拷贝和深拷贝对应的魔术方法
__copy__
、__deepcopy__
两个函数。
假设:对象包含用户名和密码两个属性信息,该对象在拷贝时,不能进行密码拷贝(注意:默认会进行所有属性的拷贝),此时就可以使用自定义对象协议来实现。
import copy class Demo: def __init__(self, username, password): self.username = username self.password = password def __repr__(self): return 'username: %s, password: %s' % (self.username, self.password) def __copy__(self): new_demo = Demo(self.username, '') return new_demo def __deepcopy__(self, memodict={}): new_demo = Demo(self.username, '') return new_demo def test(): demo1 = Demo('admin', '123456') print(demo1) # 这里会对所有属性的值进行拷贝(注意深浅拷贝问题) # 假设: 该类型对象拷贝时,默认不进行密码拷贝,应该如何实现? # 解决: 可以给 Demo 增加拷贝逻辑 demo2 = copy.copy(demo1) print(demo2) demo3 = copy.deepcopy(demo1) print(demo3) if __name__ == '__main__': test()
深拷贝时,有个额外的参数 memodict
,它可以用来处理循环引用。如下例子,node1
引用 node2
,node2
引用 node1
, 在自定义深拷贝函数中,就陷入了无限递归,致使程序报错:
RecursionError: maximum recursion depth exceeded while calling a Python object
import copy class Node: def __init__(self, name): self.name = name self.next = None def __deepcopy__(self, memodict={}): new_node = Node(self.name) # 递归拷贝其他节点 if self.next: new_node.next = copy.deepcopy(self.next, memodict) return new_node def test(): node1 = Node('node1') node2 = Node('node2') node1.next = node2 node2.next = node1 new_node = copy.deepcopy(node1) print(new_node) if __name__ == '__main__': test()
解决方法:我们可以将拷贝过的对象存储到 memodict
中,避免重复拷贝。如下代码所示:
def __deepcopy__(self, memodict={}): # 如果当前节点已被拷贝,直接返回 if id(self) in memodict: return memodict[id(self)] new_node = Node(self.name) # 当前节点 self 已经被拷贝,并记录下来 memodict[id(self)] = new_node # 递归拷贝其他节点 if self.next: new_node.next = copy.deepcopy(self.next, memodict) return new_node