Python 的 `copy` 模組
本文將介紹 Python 的 copy 模組。
聚焦於淺拷貝與深拷貝的差異,從物件複製的基本機制到在自訂類別中的應用,搭配實用範例做清楚說明。
YouTube Video
Python 的 copy 模組
Python 的 copy 模組是處理物件複製的標準模組。目標是理解淺拷貝與深拷貝的差異,並能控制自訂物件的複製行為。
基礎:什麼是淺拷貝?
此處示範淺拷貝在可變物件(如列表與字典)上的行為。淺拷貝僅複製最外層物件,內部元素的引用會被共用。
1# Example: shallow copy with lists and dicts
2import copy
3
4# A nested list
5original = [1, [2, 3], 4]
6shallow = copy.copy(original)
7
8# Mutate nested element
9shallow[1].append(99)
10
11print("original:", original) # nested change visible in original
12print("shallow: ", shallow)在這段程式碼中,最上層的列表被複製,但內部的子列表是透過引用共享,因此 shallow[1].append(99) 也會反映在 original 中。這是淺拷貝的典型行為。
什麼是深拷貝?
深拷貝會遞迴地複製物件及其所有內部引用,產生彼此獨立的物件。當你想安全地複製複雜的巢狀結構時使用它。
1# Example: deep copy with nested structures
2import copy
3
4original = [1, [2, 3], {'a': [4, 5]}]
5deep = copy.deepcopy(original)
6
7# Mutate nested element of the deep copy
8deep[1].append(100)
9deep[2]['a'].append(200)
10
11print("original:", original) # original remains unchanged
12print("deep: ", deep)在此範例中,對 deep 的變更不會影響 original。deepcopy 會複製整個物件圖,得到獨立的副本。
如何選擇使用哪一種
應根據物件的結構與你的目的來決定使用哪種複製方式。若不希望對內部可變元素的修改影響原物件,應使用 deepcopy。因為它會遞迴複製整個物件,建立完全獨立的副本。
相反地,若只需複製最外層物件,並且重視速度與記憶體效率,則 copy(淺拷貝)更為合適。
- 希望修改內部可變元素時不影響原物件 → 使用
deepcopy。 - 僅複製最外層即可,且想優先考量效能(速度/記憶體) → 使用
copy(淺拷貝)。
處理內建與不可變物件
不可變物件(例如 int、str,以及內容不可變的 tuple)通常不需要複製。copy.copy 在適當情況下可能直接回傳同一個物件。
1# Example: copying immutables
2import copy
3
4a = 42
5b = copy.copy(a)
6print(a is b) # True for small ints (implementation detail)
7
8s = "hello"
9t = copy.deepcopy(s)
10print(s is t) # True (strings are immutable)由於複製不可變物件幾乎沒有效率上的好處,因此可能會重複使用同一個物件。在應用程式設計中,這通常不需要擔心。
自訂類別:定義 __copy__ 與 __deepcopy__
如果預設的複製行為不符合你的類別需求,可以自行提供複製邏輯。若定義了 __copy__ 與 __deepcopy__,copy.copy 與 copy.deepcopy 會優先使用它們。
1# Example: customizing copy behavior
2import copy
3
4class Node:
5 def __init__(self, value, children=None):
6 self.value = value
7 self.children = children or []
8
9 def __repr__(self):
10 return f"Node({self.value!r}, children={self.children!r})"
11
12 def __copy__(self):
13 # Shallow copy: create new Node, but reuse children list reference
14 new = self.__class__(self.value, self.children)
15 return new
16
17 def __deepcopy__(self, memo):
18 # Deep copy: copy value and deepcopy each child
19 new_children = [copy.deepcopy(child, memo) for child in self.children]
20 new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21 memo[id(self)] = new
22 return new- 透過實作
__copy__和__deepcopy__,你可以彈性地控制類別特定的複製行為。例如,你可以處理某些屬性應該引用(共享)同一個物件,而其他屬性應該被複製為全新的物件的情況。 - 傳遞給
__deepcopy__的memo參數是一個字典,用於記錄在遞迴複製過程中已處理過的物件,並避免由循環引用引起的無限迴圈。
1# Build tree
2root_node = Node('root', [Node('child1'), Node('child2')])
3
4shallow_copy = copy.copy(root_node) # shallow copy
5deep_copy = copy.deepcopy(root_node) # deep copy
6
7# Modify children
8# affects both root_node and shallow_copy
9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)- 在這段程式碼中,變數
shallow_copy是透過「淺拷貝」建立的,因此只有物件的最上層被複製,而其內部所引用的物件(本例中的children列表)會與原始的root_node共享。因此,當你向shallow_copy新增一個節點時,共享的children列表會被更新,這一變更也會反映在root_node的內容中。 - 相對地,變數
deep_copy是透過「深拷貝」建立的,因此其內部結構會被遞迴複製,產生一棵與root_node完全獨立的樹。因此,即使你向deep_copy新增一個節點,也不會影響到root_node。
循環引用(遞迴物件)與 memo 的重要性
當複雜的物件圖出現自我引用(循環引用)時,deepcopy 會使用 memo 字典追蹤已複製的物件,以避免無限迴圈。
1# Example: recursive list and deepcopy memo demonstration
2import copy
3
4a = []
5b = [a]
6a.append(b) # a -> [b], b -> [a] (cycle)
7
8# deepcopy can handle cycles
9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))deepcopy在內部透過memo指向已複製的物件,即使存在循環也能安全地進行複製。當你手動實作遞迴複製時也需要類似的機制。
copyreg 與序列化風格的自訂複製(進階)
若要與其他函式庫協同註冊特殊的複製行為,可使用標準的 copyreg 模組註冊物件的(重新)建構方式。這對複雜物件與 C 擴充型別特別有用。
1# Example: using copyreg to customize pickling/copy behavior (brief example)
2import copy
3import copyreg
4
5class Wrapper:
6 def __init__(self, data):
7 self.data = data
8
9def reduce_wrapper(obj):
10 # Return callable and args so object can be reconstructed
11 return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)- 透過
copyreg,你可以提供同時影響pickle與deepcopy的重建規則。這是進階的 API,多數情況下僅實作__deepcopy__就已足夠。
實務上的注意事項與陷阱
為確保使用 copy 模組時的正確行為,有幾點重要事項。以下說明開發中常見的陷阱以及如何避免。
- 效能
deepcopy可能消耗大量記憶體與 CPU,請審慎評估是否必要。 - 處理你想共用的元素
若希望某些屬性(如大型快取)維持共用,請在
__deepcopy__中刻意避免複製其引用。 - 不可變的內部狀態 若內部持有不可變資料,可能無須複製。
- 執行緒與外部資源 無法被複製的資源(如 socket 與檔案控制代碼)要嘛複製沒有意義,要嘛會造成錯誤,因此必須在設計上避免將其作為複製目標。
實務範例:安全更新字典的模式
在更新複雜的組態物件時,此範例使用 deepcopy 來保護原始設定。
1# Example: safely update a nested configuration using deepcopy
2import copy
3
4default_config = {
5 "db": {"host": "localhost", "ports": [5432]},
6 "features": {"use_cache": True, "cache_sizes": [128, 256]},
7}
8
9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)透過 deepcopy,你可以安全地建立衍生設定,而不會冒著破壞預設設定的風險。這對於如設定檔般的巢狀可變結構特別有用。
最佳實務
要安全且有效地使用 copy 模組,記住以下實用準則很重要。
- 根據是否會變更以及變更範圍,選擇淺拷貝或深拷貝。
- 在自訂類別中實作
__copy__與__deepcopy__以達到預期行為。 - 由於深拷貝代價高昂,盡可能透過設計來降低複製的需求。可考慮採用不可變設計與顯式的 clone 方法等技巧。
- 處理循環引用時,使用
deepcopy,或手動提供類似 memo 的機制。 - 在設計時,確保不會複製檔案描述符、執行緒等外部資源。
結論
copy 模組是 Python 中用於物件複製的標準工具。正確理解淺拷貝與深拷貝的差異,並在需要時實作自訂的複製行為,能讓你安全且可預期地進行複製。在設計階段釐清是否真的需要複製、以及哪些應被共用,可避免不必要的錯誤與效能問題。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。