1. 程式人生 > >你真得理解 python 的淺拷貝和深拷貝嗎?

你真得理解 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__ 可以完成對一個物件拷貝的定製。這裡不展開了,有機會再探討自定義拷貝。