Python對象中的淺拷貝和深拷貝
Python部落(python.freelycode.com)組織翻譯,禁止轉載,歡迎轉發。
Python中的賦值語句不會創建對象的副本,而只是給對象綁定了新的名稱。對於不可變對象,這通常沒什麼區別。
但是在處理可變對象或可變對象集合時,你可能想找到一種方法來創建這些對象的「真正的副本」或「克隆」。
本質上來說就是有時候你會希望拷貝被修改時原對象不會自動修改。在這篇文章中,我將會告訴你如何在Python 3中拷貝或「克隆」對象,以及涉及的其他注意事項。
注意:本教程是用Python 3編寫的,但涉及到拷貝對象時,Python 2和3之間幾乎沒有區別。當有差異時,我會在文中指出它們。
我們先看看如何拷貝Python的內置集合。通過在現有集合上調用其工廠函數即可拷貝Python的內置可變集合(如列表,字典和集合):
但是,此方法不適用於自定義對象,並且最重要的是,它僅創建淺拷貝。對於像列表,字典和集合這樣的複合對象,淺拷貝和深拷貝之間有一個重要區別:
淺拷貝會構建一個新的集合對象,然後用原對象的子對象的引用填充它。實質上,淺拷貝只有一層。拷貝過程不會遞歸,因此不會創建子對象本身的副本。
深拷貝會遞歸拷貝過程。這意味著會首先構造一個新的集合對象,然後遞歸地填充原始對象中的子對象的副本。以這種方式拷貝對象會遍歷整個對象樹,從而創建原始對象及其所有子對象的完全獨立的副本。
我知道這有點繞口,所以讓我們通過一些例子來深入了解淺拷貝和深拷貝的區別。
創建淺拷貝在下面的例子中,我們將創建一個新的嵌套列表,然後用工廠函數list對它進行淺拷貝:
這意味著ys現在將是一個新的獨立的對象,其內容與xs一致。你可以通過檢查這兩個對象來驗證這一點:
為了確認ys真的是獨立於原對象,我們來設計一個小實驗。你可以嘗試添加一個新的子列表到原始對象(xs),然後檢查以確保此修改不會影響副本(ys):
正如你所看到的,這具有預期的效果。在「表面」級別修改列表副本完全沒有問題。
但是,由於我們只創建了原始列表的淺拷貝,因此ys仍包含對存儲在xs中的原始子對象的引用。
這些子對象沒有被複制。他們只是在複製列表中再次引用。
因此,當你修改xs中一個子對象時,此修改也會在ys中反映出來——這是因為兩個列表共享相同的子對象。這種拷貝只是一個淺層,一層的拷貝:
在上面的例子中,我們(似乎)只對xs做了一些改變。但事實證明,xs和ys索引為1的子列表都進行了修改。發生這種情況是因為我們只創建了原始列表的淺拷貝。
如果我們在第一步創建了xs的深拷貝,那麼這兩個對象將完全獨立。這是對象的淺層和深層拷貝之間的實際區別。
現在你知道了如何創建一些內置集合類的淺拷貝,並且也知道了淺拷貝和深拷貝之間的區別。我們還想知道的是:
如何創建內置集合的深拷貝?
如何創建任意對象的(淺和深)拷貝,包括自定義類?
這些問題的答案是Python標準庫中的copy模塊。該模塊提供了一個簡單的介面來創建任意Python對象的深淺拷貝。
創建深拷貝讓我們重複前面的列表複製示例,但有一個重要的區別。這次我們將使用copy模塊中定義的deepcopy函數創建一個深拷貝:
當你檢查xs和我們用copy.deepcopy創建的副本zs時,你會發現它們看起來都一樣——就像前面的例子一樣:
但是,如果你對原始對象(xs)中的某個子對象進行了修改,則會看到此修改不會影響深拷貝(zs)。
這兩個對象,原始對象和副本,這次是完全獨立的。xs被遞歸拷貝,包括它的所有子對象:
你可能需要花一些時間利用Python解釋器嘗試正確地使用這些示例。當你親身體驗這些例子後,你對拷貝的理解會更容易。
順便說一下,你還可以使用copy模塊中的函數創建淺拷貝。copy.copy函數會創建對象的淺拷貝。
如果你需要清楚地表明你正在代碼中某處創建淺拷貝,這非常有用。使用copy.copy可以讓你指出這一事實。但是,對於內置集合,只需使用列表,字典和集合的工廠函數來創建淺拷貝,這更有python的風格。
複製任意Python對象我們仍然需要回答的問題是如何創建任意對象的(深淺)拷貝,包括自定義類。現在我們來看看。
copy模塊能再次幫我們。它的copy.copy和copy.deepcopy函數可用於複製任何對象。
同樣再次,理解如何使用這些的最好方法是通過一個簡單的實驗。依然以之前的列表複製示例為例。我們首先定義一個簡單的二維點類:
我希望你們認可這很簡單。我添加了一個__repr__實現,以便我們可以輕鬆地在Python解釋器中檢查由此類創建的對象。
注意:上面的例子使用Python 3.6的f-string來構造__repr__返回的字元串。在Python 2和Python 3之前的版本中,你需要使用不同的字元串格式表達式,例如:
接下來,我們將創建一個點實例,然後使用copy模塊進行淺拷貝:
如果我們檢查原始點對象和它的(淺)拷貝的內容,我們會看到正如預期的那樣:
要記住,由於我們的點對象使用原始類型(int)作為其坐標,因此在這種情況下,淺拷貝和深拷貝之間沒有區別。但我接下來會擴展這個例子。
我們來看一個更複雜的例子。我要定義另一個類來表示二維矩形。我將使對象的層次結構更複雜——我的矩形將使用點對象來表示它們的坐標:
再次,我們首先嘗試創建一個矩形實例的淺拷貝:
如果你檢查原始矩形及其副本,你會看到__repr__重寫進行良好,並且淺拷貝過程按預期工作:
還記得前面的列表示例如何說明深和淺拷貝之間的區別嗎?我將在這裡使用相同的方法。我將在對象層次結構中的更深層修改一個對象,然後你會看到(淺層)拷貝中反映的此更改:
我希望這和你預期的一致。接下來,我將創建原始矩形的深拷貝。然後我將進行另一個修改,你會看到哪些對象受到影響:
瞧!這次深拷貝(drect)完全獨立於原始(rect)和淺拷貝(srect)。
我們已經在這裡介紹了很多內容,但關於複製對象還有一些細節。
在這個話題上深入研究是值得的,因此你可能需要研究copy模塊文檔。例如,通過定義特殊函數__copy__和__deepcopy__,對象可以控制它們如何被拷貝。
記住三件事創建對象的淺拷貝不會克隆子對象。因此,副本不完全獨立於原對象。
對象的深層副本將遞歸地拷貝子對象。拷貝完全獨立於原始文件,但創建深拷貝較慢。
可以使用copy模塊拷貝任意對象(包括自定義類)。
英文原文:https://realpython.com/blog/python/copying-python-objects/
譯者:β
※用Python編輯視頻:MoviePy
※快樂的遷移到 Python3
TAG:Python部落 |