如何理解 Python 浅拷贝和深拷贝

为了让一个对象发生改变时不对原对象产生副作用,此时,需要一份这个对象的拷贝,python 提供了 copy 机制来完成这样的任务,对应的模块是 copy

浅拷贝:shadow copy

copy 模块中,有 copy 函数可以完成浅拷贝。

1
from copy import copy

在 python 中,标识一个对象唯一身份的是:对象的id(内存地址),对象类型,对象值,而浅拷贝就是创建一个具有相同类型,相同值但不同id的新对象。

对可变对象而言,对象的值一样可能包含有对其他对象的引用,浅拷贝产生的新对象,虽然具有完全不同的id,但是其值若包含可变对象,这些对象和原始对象中的值包含同样的引用。

1
2
3
4
5
6
7
8
9
10
11
12
>>> import copy
>>> l = {'a': [1,2,3], 'b':[4,5,6]}
>>> c = copy.copy(l)
>>> id(l) == id(c)
False
>>> l['a'].append('4')
>>> c['b'].append('7')
>>> l
{'a': [1, 2, 3, '4'], 'b': [4, 5, 6, '7']}
>>> c
{'a': [1, 2, 3, '4'], 'b': [4, 5, 6, '7']}
>>>

可见浅拷贝产生的新对象中,可变对象的值在发生改变时会对原对象的值产生副作用,因为这些值是同一个引用。

浅拷贝仅仅对对象自身创建了一份拷贝,而没有在进一步处理对象中包含的值,因此使用浅拷贝的典型使用场景是:对象自身发生改变的同时需要保持对象中的值完全相同,比如 list 排序。

1
2
3
4
5
6
7
8
9
10
11
>>> def sorted_list(olist, key=None):
... copied_list = copy.copy(olist)
... copied_list.sort(key=key)
... return copied_list
...
>>> a = [3,2,1]
>>> b = sorted_list(a)
>>> a
[3, 2, 1]
>>> b
[1, 2, 3]

深拷贝:deep copy

copy 模块中,有 deepcopy 函数可以完成深拷贝。

1
from copy import deepcopy

深拷贝不仅仅拷贝了原始对象自身,也对其包含的值进行拷贝,它会递归的查找对象中包含的其他对象的引用,来完成更深层次拷贝。因此,深拷贝产生的副本可以随意修改而不需要担心会引起原始值的改变。

1
2
3
4
5
6
7
8
9
10
11
12
>>> import copy
>>> l = {'a': [1,2,3], 'b':[4,5,6]}
>>> c = copy.deepcopy(l)
>>> id(l) == id(c)
False
>>> l['a'].append('4')
>>> c['b'].append('7')
>>> l
{'a': [1, 2, 3, '4'], 'b': [4, 5, 6]}
>>> c
{'a': [1, 2, 3], 'b': [4, 5, 6, '7']}
>>>

值得注意的是,深拷贝并非完完全全递归查找所有对象,因为一旦对象引用了自身,完全递归可能会导致无限循环。一个对象被拷贝了,python 会对该对象做个标记,如果还有其他需要拷贝的对象引用着该对象,它们的拷贝其实指向的是同一份拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> a = [1,2]
>>> b = [a,a]
>>> b
[[1, 2], [1, 2]]
>>> c = deepcopy(b)
>>> id(b[0]) == id(c[0])
False
>>> id(b[0]) == id(b[1])
True
>>> c
[[1, 2], [1, 2]]
>>> c[0].append(3) #c list 中包含的两份拷贝指向同一处
>>> c
[[1, 2, 3], [1, 2, 3]]
>>>

自定义拷贝机制

使用 _copy___deepcopy__ 可以完成对一个对象拷贝的定制。这里不展开了,有机会再探讨自定义拷贝。

三月沙 wechat
扫描关注 wecatch 的公众号