1. 程式人生 > >深copy與淺copy

深copy與淺copy

lib 意義 blog 數據 hat itl 了解 ble copies

copydeep copy 是前兩天讓我特別迷惑的兩個 Python 概念。今天下決心花時間搞懂了兩者的區別,更重要的是通過它們認識了 Python 存儲數據的一些有趣特點。

技術分享圖片

雖然第一次聽說 Python 中的 copydeep copy 是作為 copy 模塊中的兩個 method。但它們其實是 OOP 語言中常見的概念。這裏只說 Python,其他語言不了解。

Pythoncopy 模塊中的 copy() method 其實是與 deep copy 相對的 shallow copycopy.copy(object) 就等於是對 object 做了 shallow copy

先說結論:

  • 對於簡單的 object,用 shallow copydeep 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。

當在 Pythona = 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]

而在此過程當中,ab 都沒有動,他們還貼在那個物品上。因此自然 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