發(fā)布于:2021-02-01 14:28:20
0
499
0
Python中的賦值語句不創(chuàng)建對象的副本,它們只將名稱綁定到對象。對于不可變對象,這通常沒有什么區(qū)別。
但是對于處理可變對象或可變對象的集合,您可能正在尋找一種方法來創(chuàng)建這些對象的“真實副本”或“克隆”。
本質(zhì)上,您有時需要可以修改的副本,而無需同時自動修改原始副本。在本文中,我將向您簡要介紹如何在python3中復制或“克隆”對象,以及其中涉及的一些注意事項。
注意:本教程是用Python3編寫的,但在復制對象方面,Python2和Python3沒有什么區(qū)別。如果有差異,我會在文中指出。
讓我們先看看如何復制Python的內(nèi)置集合。Python內(nèi)置的可變集合(如list、dict和set)可以通過在現(xiàn)有集合上調(diào)用它們的工廠函數(shù)來復制:
new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)
但是,這種方法不適用于自定義對象,除此之外,它只創(chuàng)建淺復制。對于列表、dict和set等復合對象,淺復制和深復制有一個重要區(qū)別:
淺復制意味著構(gòu)造一個新的集合對象,然后用在原始集合中找到的子對象的引用填充它。從本質(zhì)上說,淺復制只是一個層次的深復制。復制過程不會遞歸,因此不會創(chuàng)建子對象本身的副本。
深度復制使復制過程遞歸。這意味著首先構(gòu)造一個新的集合對象,然后用在原始集合中找到的子對象的副本遞歸地填充它。以這種方式復制對象會遍歷整個對象樹,以創(chuàng)建原始對象及其所有子對象的完全獨立克隆。
我知道,那有點過分了。所以讓我們看一些例子來說明深復制和淺復制之間的區(qū)別。
進行淺復制
在下面的示例中,我們將創(chuàng)建一個新的嵌套列表,然后使用list()
工廠函數(shù)對其進行淺復制:
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys = list(xs) # Make a shallow copy
這意味著ys
現(xiàn)在將是一個新的獨立對象,其內(nèi)容與xs
相同。您可以通過檢查兩個對象來驗證這一點:
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
要確認ys
確實獨立于原始對象,讓我們設計一個小實驗。您可以嘗試將新的子列表添加到原始列表(xs
),然后檢查以確保此修改不會影響副本(ys
):
>>> xs.append(['new sublist'])
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
如您所見,這達到了預期的效果。在“淺表”級別修改復制的列表完全沒有問題。
但是,由于我們只創(chuàng)建了原始列表的淺表副本,ys
仍然包含對存儲在xs
中的原始子對象的引用這些子對象沒有被復制。它們只是在復制的列表中再次引用。
因此,當您修改xs
中的一個子對象時,這種修改也會反映在ys
中,這是因為兩個列表共享相同的子對象。副本只是一個淺的、一級的深副本:
>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
在上面的示例中,我們(似乎)只對xs
進行了更改。但事實證明,在xs
和ys
中,索引1處的兩個子列表都被修改了。再次發(fā)生這種情況是因為我們只創(chuàng)建了原始列表的淺副本。
如果我們在第一步中創(chuàng)建了xs
的深副本,兩個對象將完全獨立。這就是對象的淺復制和深復制之間的實際區(qū)別。
現(xiàn)在您知道如何創(chuàng)建一些內(nèi)置集合類的淺復制,并且知道了淺復制和深復制之間的區(qū)別。我們?nèi)匀恍枰卮鸬膯栴}是:
如何創(chuàng)建內(nèi)置集合的深度副本?
如何創(chuàng)建任意對象(包括自定義類)的副本(淺副本和深副本)?
這些問題的答案在Python標準庫的copy
模塊中。此模塊提供了一個簡單的界面,用于創(chuàng)建任意Python對象的淺復制和深復制。
制作深度復制
讓我們重復前面的列表復制示例,但有一個重要區(qū)別。這次我們將使用copy
模塊中定義的deepcopy()
函數(shù)來創(chuàng)建深度副本:
>>> import copy
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs = copy.deepcopy(xs)
當您檢查xs
及其用copy.deepcopy()
創(chuàng)建的克隆zs
時,您將看到它們看起來又是一樣的上一個示例:
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
但是,如果您對原始對象(xs
)中的一個子對象進行修改,您將看到此修改不會影響深度副本(zs
)。
這一次,原始對象和副本都是完全獨立的。xs
是遞歸克隆的,包括它的所有子對象:
>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
您現(xiàn)在可能需要花一些時間坐下來使用Python解釋器并播放這些示例。當你開始親身體驗和體驗這些例子時,你可以更輕松地將你的頭腦圍繞在復制對象上。
順便說一句,你也可以使用copy
模塊中的函數(shù)創(chuàng)建淺復制。copy.copy()
函數(shù)創(chuàng)建對象的淺復制。
如果您需要清楚地告知您正在代碼中的某個地方創(chuàng)建淺復制,這將非常有用。使用copy.copy()
可以指示此事實。但是,對于內(nèi)置的集合,只需使用list、dict和set factory函數(shù)來創(chuàng)建淺層副本就被認為更具Pythonic了。
復制任意Python對象
我們?nèi)匀恍枰卮鸬膯栴}是如何創(chuàng)建任意對象(包括自定義類)的副本(淺副本和深副本)?,F(xiàn)在讓我們來看看。
再一次,copy
模塊來拯救我們。它的copy.copy()
和copy.deepcopy()
功能可用于復制任何對象。
再次強調(diào),了解如何使用這些功能的最佳方法是通過簡單的實驗。我將以前面的列表復制示例為基礎。讓我們從定義一個簡單的2D點類開始:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point({self.x!r}, {self.y!r})'
我希望您同意這非常簡單。我添加了一個__repr__()
實現(xiàn),這樣我們就可以在Python解釋器中輕松地檢查從這個類創(chuàng)建的對象。
注意:上面的示例使用python3.6f-string來構(gòu)造__repr__
返回的字符串。在Python2和Python3.6之前的版本上,您將使用不同的字符串格式表達式,例如:
def __repr__(self):
return 'Point(%r, %r)' % (self.x, self.y)
接下來,我們將創(chuàng)建一個Point
實例,然后(粗略地)復制它,使用copy
模塊:
>>> a = Point(23, 42)
>>> b = copy.copy(a)
如果我們檢查原始Point
對象及其(淺層)克隆的內(nèi)容,我們會看到預期的結(jié)果:
>>> a
Point(23, 42)
>>> b
Point(23, 42)
>>> a is b
False
這里還有一些需要記住的內(nèi)容。因為我們的點對象使用不可變類型(int)作為其坐標,所以在這種情況下淺復制和深復制沒有區(qū)別。但我馬上就要展開這個例子了。
讓我們轉(zhuǎn)到一個更復雜的例子。我將定義另一個類來表示二維矩形。我將用一種方法來創(chuàng)建一個更復雜的對象層次結(jié)構(gòu)我的矩形將使用Point
對象來表示它們的坐標:
class Rectangle:
def __init__(self, topleft, bottomright):
self.topleft = topleft
self.bottomright = bottomright
def __repr__(self):
return (f'Rectangle({self.topleft!r}, '
f'{self.bottomright!r})')
再次,首先我們將嘗試創(chuàng)建一個矩形實例的淺層副本:
rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)
如果您檢查原始矩形及其副本,您將看到__repr__()
重寫工作得多么順利,淺復制過程也按預期工作:
>>> rect
Rectangle(Point(0, 1), Point(5, 6))
>>> srect
Rectangle(Point(0, 1), Point(5, 6))
>>> rect is srect
False
還記得前面的列表示例如何說明深復制和淺復制之間的區(qū)別嗎?我要用同樣的方法。我將在對象層次結(jié)構(gòu)中更深層地修改一個對象,然后您將在(淺層)副本中看到這一更改:
>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))
我希望這是您所期望的。接下來,我將創(chuàng)建原始矩形的深度副本。然后我將應用另一個修改,您將看到哪些對象受到影響:
>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6))
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))
Voila!這一次,深復制(drect
)完全獨立于原始拷貝(rect
)和淺復制(srect
)。
我們在這里討論了很多問題,復制對象還有一些更精細的地方。
深入是值得的(哈?。┰诒局黝}中,您可能需要學習copy
模塊文檔。例如,對象可以通過在其上定義特殊方法__copy__()
和__deepcopy__()
來控制它們的復制方式。
記住的3件事
對對象進行淺層復制不會克隆子對象。因此,副本并非完全獨立于原始對象。
對象的深度副本將遞歸地克隆子對象。克隆完全獨立于原始副本,但創(chuàng)建深度副本的速度較慢。
您可以使用copy
模塊復制任意對象(包括自定義類)。