Python 擴充之詳解深拷貝和淺拷貝

Rocky0429發表於2018-12-08

正式開始

首先我在這介紹兩個新的小知識,要在下面用到。一個是函式 id() ,另一個是運算子 is。id() 函式就是返回物件的記憶體地址;is 是比較兩個變數的物件引用是否指向同一個物件,在這裡請不要和 == 混了,== 是比較兩個變數的值是否相等。

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> id(a)
38884552L
>>> a is b
False
>>> a == b
True

copy 這個詞有兩種叫法,一種是根據它的發音音譯過來的,叫拷貝;另一種就是標準的翻譯,叫複製。

其實單從表面意思來說,copy 就是將某件東西再複製一份,但是在很多程式語言中,比如 Python,C++中,它就不是那麼的簡單了。

>>> a = 1
>>> b = a
>>> b
1

看到上面的例子,從表面上看我們似乎是得到了兩個 1,但是如果你看過我之前寫的文章,你應該對一句話有印象,那就是 “變數無型別”, Python 中變數就是一個標籤,這裡我們有請 id() 閃亮登場,看看它們在記憶體中的位置。

>>> a = 1
>>> b = a
>>> b
1
>>> id(a)
31096808L
>>> id(b)
31096808L

看出來了嗎,id(a) 和 id(b) 相等,所以並沒有兩個 1,只是一個 1 而已,只不過是在 1 上貼了兩張標籤,名字是 a 和 b 罷了,這種現象普遍存在於 Python 之中,這種賦值的方式實現了 “假裝” 拷貝,真實的情況還是兩個變數和同一個物件之間的引用關係。

我們再來看 copy() 方法:

>>> a = {`name`:`rocky`,`like`:`python`}
>>> b = a.copy()
>>> b
{`name`: `rocky`, `like`: `python`}
>>> id(a)
31036280L
>>> id(b)
38786728L

咦,果然這次得到的 b 和原來的 a 不同,它是在記憶體中又開闢了一個空間。那麼我們這個時候就來推理了,雖然它們兩個是一樣的,但是它們在兩個不同的記憶體空間裡,那麼肯定彼此互不干擾,如果我們去把 b 改了,那麼 a 肯定不變。

>>> b[`name`] = `leey`
>>> b
{`name`: `leey`, `like`: `python`}
>>> a
{`name`: `rocky`, `like`: `python`}

結果和我們上面推理的一模一樣,所以理解了物件有型別,變數無型別,變數是物件的標籤,就能正確推斷出 Python 提供的結果。

我們接下來在看一個例子,請你在往下看的時候保證上面的你已經懂了,不然容易暈車。

>>> a = {`name`:`rocky`,`like`:`python`}
>>> b = a
>>> b
{`name`: `rocky`, `like`: `python`}
>>> b[`name`] = `leey`
>>> b
{`name`: `leey`, `like`: `python`}
>>> a
{`name`: `leey`, `like`: `python`}

上面的例子看出什麼來了嗎?修改了 b 對應的字典型別的物件,a 的物件也變了。也就是說, b = a 得到的結果是兩個變數引用了同一個物件,但是事情真的這麼簡單嗎?請睜大你的眼睛往下看,重點來了。

>>> first = {`name`:`rocky`,`lanaguage`:[`python`,`c++`,`java`]}
>>> second = first.copy()
>>> second
{`name`: `rocky`, `lanaguage`: [`python`, `c++`, `java`]}
>>> id(first)
31036280L
>>> id(second)
38786728L

在這裡的話沒有問題,和我們之前說的一樣,second 是從 first 拷貝過來的,它們分別引用的是兩個物件。

>>> second[`lanaguage`].remove(`java`)
>>> second
{`name`: `rocky`, `lanaguage`: [`python`, `c++`]}
>>> first
{`name`: `rocky`, `lanaguage`: [`python`, `c++`]}

發現什麼了嗎?按理說上述例子中 second 的 lanaguage 對應的是一個列表,我刪除這個列表裡的值,也只應該改變的是 second 啊,為什麼連 first 的也會改,不是應該互不干擾嗎?是不是很意外?是我們之前說的不對嗎?那我們再試試另一個鍵:

>>> second[`name`] = `leey`
>>> second
{`name`: `leey`, `lanaguage`: [`python`, `c++`]}
>>> first
{`name`: `rocky`, `lanaguage`: [`python`, `c++`]}

前面說的原理是有效的,那這到底是為什麼啊,來來來,有請我們的 id() 再次閃亮登場。

>>> id(first[`name`])
38829152L
>>> id(second[`name`])
38817544L
>>> id(first[`lanaguage`])
38754120L
>>> id(second[`lanaguage`])
38754120L

其實這裡深層次的原因是和 Python 的儲存資料的方式有關,這裡不做過多的說明(其實是我也不懂。。 在這裡,我們只需要知道的是,當 copy() 的時候,列表這類由字串,數字等複合而成的物件仍然是複製了引用,也就是貼標籤,並沒有建立一個新的物件,我們把這種拷貝方式叫做淺拷貝(唉呀媽呀,終於把這個概念引出來了。。,言外之意就是並沒有解決深層次的問題,再言外之意就是還有能夠解決深層次問題的方法。

確實,在 Python 中還有一個深拷貝(deep copy),在使用它之前要引入一個 copy 模組,我們來試一下。

>>> import copy
>>> first = {`name`:`rocky`,`lanaguage`:[`python`,`c++`,`java`]}
>>> second = copy.deepcopy(first)
>>> second
{`name`: `rocky`, `lanaguage`: [`python`, `c++`, `java`]}
>>> second[`lanaguage`].remove(`java`)
>>> second
{`name`: `rocky`, `lanaguage`: [`python`, `c++`]}
>>> first
{`name`: `rocky`, `lanaguage`: [`python`, `c++`, `java`]}

用了深拷貝以後,果然就不是引用了。

寫在最後

更多內容,歡迎關注公眾號「Python空間」,期待和你的交流。

相關文章