BG74 應當瞭解的Python拷貝

大體上來看,拷貝可以分為兩種:深拷貝和淺拷貝

所謂“深拷貝”,是指經過該操作得到的數據與原始數據內容一致,但存儲空間不同;所謂“淺拷貝”是指該操作所得數據與原始數據存儲在同一地址,只是換了一個別名。按照邏輯可知,深拷貝會比較耗時,因為需要對數據內容逐個複製,但是能夠保證在操作備份數據的時候不影響原始數據;淺拷貝通常只是修改別名或者得到指針,不需要對原始數據逐個複製,因此速度快,但對備份的修改直接影響原始數據

在任何一種編程語言中,都會支持對數據的拷貝,但是不同的編程語言對拷貝支持的程度不一樣。比如,在C/C++中,一般的值傳遞都是深拷貝,而傳遞指針或者引用都是淺拷貝(針對指針本身的複製除外)。而在Python中,這個問題稍微有點複雜,本文就是為了說清楚Python中的拷貝到底是怎麼回事,以及我們該如何使用拷貝操作。

1. 基礎類型的拷貝

Python中的基礎類型包括bool,int,float,str等類型。通常我們在使用賦值運算符處理這些類型的時候很少去考慮究竟發生了什麼,下面的例子可以告訴我們一些信息。

<code>a = 1
b = a
print('id(a)={}, id(b)={}'.format(id(a), id(b)))/<code>

Output:

<code>id(a)=4309896976, id(b)=4309896976/<code>

從拷貝操作的輸入和輸出的id可以看出:Python中的int類型,乃至所有類型,直接使用賦值運算符進行的拷貝都是淺拷貝。既然是淺拷貝,我們是否需要擔心對備份數據的寫操作會汙染原始數據呢?先說一下答案:不會。下面的例子可以支持這一說法。

<code>b = 2
print('a={}, b={}'.format(a, b))/<code>

Output:

<code>a=1, b=2/<code>

明明是淺拷貝,但是對備份數據的修改為什麼不會影響原始數據呢?這是因為Python中使用賦值運算符的時候,都會修改被賦值變量的指向。上面的例子中,b=2這一行所進行的操作是將 “b” 變量修改為整數 “2”存儲的地址別名,而不是將“b”原先指向的內容修改為新的。所以,雖然基本類型的複製是“淺拷貝”,但是我們可以當成“深拷貝”來使用。

這樣的話,又產生了一個問題:str類型的賦值又是怎樣的呢?str與int等其他基本類型不一樣,它有子元素,那麼對備份數據的這些子元素的修改會不會汙染原始數據呢?下面的例子將說明這個問題。

<code>c = '123'
c[1] = '4'
print(c)/<code>

Output:

<code>---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-e0ac949badbe> in <module>
1 c = '123'
----> 2 c[1] = '4'
3 print(c)

TypeError: 'str' object does not support item assignment/<module>/<ipython-input-5-e0ac949badbe>/<code>

這個報錯說明了Python不允許修改str的子元素,即所有的str對象都是常量。這一規則將str與int等其他基本類型在拷貝這個操作上保持了一致。所以還是那個結論:基本數據類型的賦值操作實質上是淺拷貝,但是可以當成深拷貝使用,即對備份數據的修改不會影響原始數據。

2. 複合類型淺拷貝

事實上,在Python中,賦值運算符不僅對基本類型是淺拷貝,對所有所有類型都是淺拷貝

。這裡我們以list類型為例來說明一下這個規則。

<code>list_a = [1,2,3,4,5]
list_b = list_a
print(id(list_a))
print(id(list_b))/<code>

Output:

<code>4353845920
4353845920/<code>

複製操作的輸入和輸出對象id相同,表明確實是淺拷貝。接下來,我們修改備份數據的子元素,看看會發生什麼情況。

<code>list_b[1] = 6
print('list_a={}'.format(list_a))
print('list_b={}'.format(list_b))/<code>

Output:

<code>list_a=[1, 6, 3, 4, 5]
list_b=[1, 6, 3, 4, 5]/<code>

修改“list_b”變量的子元素,“list_a”變量對應的子元素也發生了變化,這是符合淺拷貝的規則的。

3. 複合類型深拷貝

有時我們需要對符合類型使用深拷貝,那該怎樣操作呢?還是以list類型為例。

<code>list_a = [1,2,3,4,5]
list_b = list(list_a)
print(id(list_a))
print(id(list_b))/<code>

Output:

<code>4355914880
4356592656/<code>

使用構造函數來完成深拷貝當然是一個方法,但是並不是所有的複合類型都提供了深拷貝的構造函數。如果不能使用構造函數完成,那麼又該如何實現呢?copy模塊是一個不錯的助手。

<code>import copy
list_b = copy.copy(list_a)
print(id(a))
print(id(b))/<code>

Output:

<code>4309896976
4309897008/<code>

我們可以使用copy.copy()函數完成一般複合對象的深拷貝。但是拷貝這個事情到此並沒有結束,下面還有一些詭異的現象。

4. 多層複合類型的拷貝

所謂多層複合類型,就是指複合類型的子元素也是複合類型。在這種情況下,使用構造函數或者copy.copy()函數能夠完成深拷貝嗎?

<code>list_a = [[1,2], [3,4], [5,6]]
list_b = list(list_a)
print(id(list_a))

print(id(list_b))/<code>

Output:

<code>4315048128
4354396480/<code>

使用構造函數完成拷貝之後,原始數據和備份數據的id不相同,可以認為是深拷貝,但是別高興得太早。

<code>list_b[0][0] = 100
print(list_a)/<code>

Output:

<code>[[100, 2], [3, 4], [5, 6]]/<code>

這個實驗的結果有點詭異,說好了是深拷貝,怎麼會修改備份數據之後,原始數據發生了同樣的改變?到這一步,我們不得不查看一下兩者的子元素是否指向同一地址。

<code>print(id(list_a[0]))
print(id(list_b[0]))/<code>

Output:

<code>4489536608
4489536608/<code>

到這裡我們才明白,原來構造函數這種方式的拷貝並不會對子元素進行深拷貝,那麼copy.copy()函數能否完成子元素的深拷貝呢?

<code>import copy
list_b = copy.copy(list_a)
print(id(list_a[0]))

print(id(list_b[0])/<code>

Output:

<code>4489536608
4489536608/<code>

這個實驗結果表明,copy.copy()函數也無法完成子元素的深拷貝。難道Python就沒有辦法實現徹底的深拷貝嗎?非也,copy模塊中還有一個神奇的函數deepcopy()可以幫助我們。

<code>import copy
list_b = copy.deepcopy(list_a)
print(id(list_a[0]))
print(id(list_b[0]))/<code>

Output:

<code>4489536608
4495039056/<code>

以上實驗表明,子元素存儲數據的地址不同,已經完成了子元素的深拷貝。我們還是使用最原始的方法檢驗一下,修改備份數據的子對象試試。

<code>list_b[0][0] = -100
print(list_a)/<code>

Output:

<code>[[100, 2], [3, 4], [5, 6]]/<code>

可以看到,原始數據對應位置的數據沒有被修改,copy.deepcopy()函數完成的是完全的深拷貝操作

總結一下Python中的拷貝:

  1. 賦值運算符所進行的拷貝都是淺拷貝,但是對於基礎類型可以當成深拷貝使用
  2. 對於單層的複合類型,可以使用其構造函數或者copy.copy()完成深拷貝操作
  3. 對於多層複合類型,只能使用copy.deepcopy()完成深拷貝操作

最後再多說一句,深拷貝雖然安全,但是對速度影響很大,所以一般情況下都不要使用深拷貝操作,除非需要對原始數據進行保護。瞭解了Python的拷貝機制,再也不擔心寫壞內存了。


分享到:


相關文章: