深copy與淺copy
copy 和 deep copy 是前兩天讓我特別迷惑的兩個 Python 概念。今天下決心花時間搞懂了兩者的區別,更重要的是通過它們認識了 Python 存儲數據的一些有趣特點。
雖然第一次聽說 Python 中的 copy 與 deep copy 是作為 copy
模塊中的兩個 method。但它們其實是 OOP 語言中常見的概念。這裏只說 Python,其他語言不了解。
Python 的 copy 模塊中的
copy()
method 其實是與 deep copy 相對的 shallow copy。copy.copy(object)
就等於是對 object 做了 shallow copy。
先說結論:
- 對於簡單的 object,用 shallow copy 和 deep copy 沒區別:
>>> import copy
>>> origin = 1
>>> cop1 = copy.copy(origin)
#cop1 是 origin 的shallow copy
>>> cop2 = copy.deepcopy(origin)
#cop2 是 origin 的 deep copy
>>> origin = 2
>>> origin
2
>>> cop1
1
>>> cop2
1
#cop1 和 cop2 都不會隨著 origin 改變自己的值
>>> cop1 == cop2
True
>>> cop1 is cop2
True
- 復雜的 object, 如 list 中套著 list 的情況,shallow copy 中的 子list,並未從原 object 真的「獨立」出來。
也就是說,如果你改變原 object 的子 list 中的一個元素,你的 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
可以看到 cop1
,也就是 shallow copy 跟著 origin 改變了。而 cop2
,也就是 deep copy 並沒有變。
似乎 deep copy 更加符合我們對「復制」的直覺定義: 一旦復制出來了,就應該是獨立的了。如果我們想要的是一個字面意義的「copy」,那就直接用 deep_copy
即可。
那麽為什麽會有 shallow copy 這樣的「假」 copy 存在呢? 這就是有意思的地方了。
Python 與眾不同的變量儲存方法
Python 存儲變量的方法跟其他 OOP 語言不同。它與其說是把值賦給變量,不如說是給變量建立了一個到具體值的 reference。
當在 Python 中 a = something
應該理解為給 something 貼上了一個標簽 a。當再賦值給 a
的時候,就好象把 a 這個標簽從原來的 something 上拿下來,貼到其他對象上,建立新的 reference。 這就解釋了一些 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]
);而第二段則是把 list 中每個元素分別改變。
而對 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]
上,既然這個 reference 也沒有改變過。 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]
。
這部分搞明白了之後再去看 copy 的區別就容易多了。
言歸正傳,Copy時候到底發生了什麽
最初對 copy 產生疑惑,是有一次想對一個復雜的 list 遍歷並且做修改。
這種情況下,最好先建立一個 copy 出來:
If you need to modify the sequence you are iterating over while inside the loop (for example to duplicate selected items), it is recommended that you first make a copy. Iterating over a sequence does not implicitly make a copy.
– Python Documentation
於是想當然用了 copy.copy()
。結果卻發現本體與 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
官方解釋是這樣的:
The difference between shallow and deep copying is only relevant for compound objects (objects that contain other objects, like lists or class instances):
A shallow copy constructs a new compound object and then (to the extent possible) inserts references into it to the objects found in the original.
A deep copy constructs a new compound object and then, recursively, inserts copies into it of the objects found in the original.
兩種 copy 只在面對復雜對象時有區別,所謂復雜對象,是指對象中含其他對象(如復雜的 list 和 class)。
由 shallow copy 建立的新復雜對象中,每個子對象,都只是指向自己在原來本體中對應的子對象。而 deep copy 建立的復雜對象中,存儲的則是本體中子對象的 copy,並且會層層如此 copy 到底。
– Python Doctumentation
這個解釋看上去略抽象。
先看這裏的 shallow copy。 如圖所示,cop1 就是給當時的 origin 建立了一個鏡像。origin 當中的元素指向哪, cop1 中的元素就也指向哪。這就是官方 doc 中所說的 inserts references into it to the objects found in the original
。
這裏的關鍵在於,origin[2]
,也就是 [3, 4] 這個 list。根據 shallow copy 的定義,在 cop1[2]
指向的是同一個 list [3, 4]。那麽,如果這裏我們改變了這個 list,就會導致 origin 和 cop1 同時改變。這就是為什麽上邊 origin[2][0] = "hey!"
之後,cop1 也隨之變成了 [1, 2, [‘hey!‘, 4]]
。
再來看 deep copy。 從圖中可以看出,cop2 是把 origin 每層都 copy 了一份存儲起來。這時候的 origin[2]
和 cop2[2]
雖然值都等於 [3, 4],但已經不是同一個 list了。
既然完全獨立,那無論如何改變其中一個,另一個自然不會隨之改變。
深copy與淺copy