Python 的 `copy` 模組

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 的變更不會影響 originaldeepcopy 會複製整個物件圖,得到獨立的副本。

如何選擇使用哪一種

應根據物件的結構與你的目的來決定使用哪種複製方式。若不希望對內部可變元素的修改影響原物件,應使用 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.copycopy.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,你可以提供同時影響 pickledeepcopy 的重建規則。這是進階的 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 頻道。

YouTube Video