圖解 Python 淺拷貝與深拷貝

AIBigbull2050發表於2019-08-27
Python 中的賦值語句不會建立物件的拷貝,僅僅只是將名稱繫結至一個物件。對於不可變物件,通常沒什麼差別,但是處理可變物件或可變物件的集合時,你可能需要建立這些物件的 “真實拷貝”,也就是在修改建立的拷貝時不改變原始的物件。


本文將以圖文方式介紹 Python 中複製或“克隆”物件的操作。

首先介紹一下 Python 中淺拷貝與深拷貝的區別:

  • 淺拷貝:淺拷貝意味著構造一個新的集合物件,然後用原始物件中找到的子物件的引用來填充它。從本質上講,淺層的複製只有一層的深度。複製過程不會遞迴,因此不會建立子物件本身的副本。
  • 深拷貝:深拷貝使複製過程遞迴。這意味著首先構造一個新的集合物件,然後遞迴地用在原始物件中找到的子物件的副本填充它。以這種方式複製一個物件,遍歷整個物件樹,以建立原始物件及其所有子物件的完全獨立的克隆。


賦值與引用

在開始淺拷貝與深拷貝前,我們先來看一下 Python 中的賦值與引用。

lst = [1, 2, 3]

new_list = lst

從字面上看,上述語句建立了變數 lst 和 new_list,並且 lst 和 new_list 的賦值都為一個列表。但是,Python 的賦值語句並不會複製物件,而是會重新建立一個物件的引用。


圖解 Python 淺拷貝與深拷貝


可以看出,lst 和 new_list 都引用了同一個列表。

建立淺拷貝

不少教程裡都會提到,如果你有一個列表,當你想要修改列表中的值但卻不想影響原始物件時,可以使用 list 複製(淺拷貝)一個列表。

我們先來試一下:

lst = [1, 2, 3]

new_list = list(lst)


圖解 Python 淺拷貝與深拷貝


沒錯,lst 和 new_list 分別指向了不同的列表。當修改 lst 列表中的值時,並不會對 new_list 物件產生影響。

lst[0] = 'x'

print(lst)
print(new_list)


['x', 2, 3]

[1, 2, 3]



圖解 Python 淺拷貝與深拷貝


之所以說 list 語句是淺拷貝,是因為這種修改只對一層物件有效,當列表中有子物件時,對子物件的修改將影響原始物件和淺拷貝物件。

為了解釋這一說法,讓我們先建立一個巢狀列表,並使用 list 函式建立淺拷貝。

lst = [[1, 2, 3], [4, 5, 6]]

new_list = list(lst)

這裡 new_list 是有著和 lst 一樣內容的新的獨立的物件。


圖解 Python 淺拷貝與深拷貝


可以看到 lst 和 new_list 分別指向了不同的物件。

對第一層 lst 的修改,將不會對 new_list 副本造成影響。

lst.append([7, 8, 9])

print(lst)
print(new_list)


[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

[[1, 2, 3], [4, 5, 6]]



圖解 Python 淺拷貝與深拷貝


但是,因為我們只建立了原始列表的一個淺拷貝,所以 new_list 仍然包含對 lst 中儲存的原始子物件的引用。

也就是如上圖所示,lst 和 new_list 的子列表都指向了相同的物件。

子物件沒有被複制,它們只是在複製的列表中被再次引用。

因此,當你修改 lst 中的一個子物件時,這種修改也會反映到 new_list 中—— 這是因為兩個列表共享相同的子物件。這種複製只是一個淺的,一個層級的複製:

lst[0][0] = 'x'

print(lst)
print(new_list)


[['x', 2, 3], [4, 5, 6], [7, 8, 9]]

[['x', 2, 3], [4, 5, 6]]



圖解 Python 淺拷貝與深拷貝


如果我們在第一步中建立了一個 lst 的深拷貝,那麼兩個物件就完全獨立了。這是物件的淺拷貝和深拷貝之間的實際區別。

使用 Python 標準庫中的 copy 模組可以建立深拷貝,這個模組為建立任意 Python 物件的淺拷貝和深拷貝提供了一個簡單的介面。

建立深拷貝

這次我們使用 deepcopy() 函式建立一個物件的深拷貝:

import copy

lst = [[1, 2, 3], [4, 5, 6]]
new_list = copy.deepcopy(lst)



圖解 Python 淺拷貝與深拷貝


從圖中可以看出 lst 和 new_list 中的子物件指向了不同的物件,如果對 lst 的子物件進行修改,將不會影響 new_list。

這一次,原始物件和複製物件都是完全獨立的。如前面所說,遞迴克隆了 lst,包括它的所有子物件:

lst[0][0] = 'x'

print(lst)
print(new_list)


[['x', 2, 3], [4, 5, 6]]

[[1, 2, 3], [4, 5, 6]]



圖解 Python 淺拷貝與深拷貝


copy 模組中的 copy.copy() 函式也可以建立物件的淺拷貝。使用 copy.copy() 可以明確地表示建立淺拷貝。對於內建集合,簡單地使用 list、dict 和 set 等工廠函式來建立淺拷貝是更加 Pythonic 的。

複製任意 Python 物件

copy.copy() 和 copy.deepcopy() 函式可用於複製任意物件。以前面的列表複製示例為基礎。讓我們從定義一個簡單的 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__() 函式使我們可以輕鬆地在 Python 直譯器中檢查從這個類建立的物件。

接下來,我們將建立一個 Point 例項,然後使用 copy 模組複製(淺拷貝)它:

a = Point(23, 42)

b = copy.copy(a)
print(a is b)


False



圖解 Python 淺拷貝與深拷貝


a 和 b 分別指向了不同的 Point 例項。因為我們的 Point 物件使用不可變型別(int)作為其座標,所以在這種情況下,淺拷貝和深拷貝沒有區別。但我馬上會展開這個例子。

接下來定義另一個類來表示 2D 矩形。矩形將使用 Point 物件來表示它們的座標:

class Rectangle:

def __init__(self, topleft, bottomright):
self.topleft = topleft
self.bottomright = bottomright
def _repr__(self):
return (f'Rectangle({self.topleft!r}, {self.bottomright!r})')
# 建立一個 Rectangle 例項的淺拷貝
rect = Rectangle(Point(0, 1), Point(5, 6))
shallow_rect = copy.copy(rect)
print(rect)
print(shallow_rect)
print(rect is shallow_rect)


Rectangle(Point(0, 1), Point(5, 6))

Rectangle(Point(0, 1), Point(5, 6))
False



圖解 Python 淺拷貝與深拷貝


跟前面 list 的例子一樣,rect 和 shallow_rect 的子物件都有相同的引用。在物件層級中修改一個物件,將看到這個變化也反映在淺拷貝的副本中:

rect.topleft.x = 999

print(rect)
print(shallow_rect)


Rectangle(Point(999, 1), Point(5, 6))

Rectangle(Point(999, 1), Point(5, 6))


接下來建立 Rectangle 的深拷貝並對其進行修改:

deep_rect = copy.deepcopy(rect)

deep_rect.topleft.x = 222
print(rect)
print(shallow_rect)
print(deep_rect)


Rectangle(Point(999, 1), Point(5, 6))

Rectangle(Point(999, 1), Point(5, 6))
Rectangle(Point(222, 1), Point(5, 6))



圖解 Python 淺拷貝與深拷貝


可以看出,深拷貝完全獨立於原始物件和淺拷貝物件。

參閱 copy 模組文件 可以對複製進行進一步的研究。例如,物件可以通過定義特殊的方法 __copy__() 和 __deepcopy__() 來控制如何複製它們。

謹記三件事

  • 建立物件的淺拷貝不會克隆子物件。因此,拷貝不會完全獨立於原始物件。
  • 一個物件的深拷貝會遞迴地克隆子物件。克隆物件完全獨立於原始物件,但是建立深拷貝速度較慢。
  • 可以使用 copy 模組複製任意物件(包括自定義類)。



https://www.toutiao.com/a6729394838715761164/



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946223/viewspace-2655035/,如需轉載,請註明出處,否則將追究法律責任。

相關文章