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 以及内容不可变的元组)通常不需要复制。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__ 中有意避免复制它们的引用。
  • 不可变的内部状态 如果内部持有的是不可变数据,可能无需复制。
  • 线程与外部资源 无法复制的资源(如套接字、文件句柄)要么复制没有意义,要么会导致错误,因此在设计上必须避免对它们进行复制。

实践示例:安全更新字典的模式

在更新复杂的配置对象时,本示例使用 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__,以获得预期的行为。
  • 由于深拷贝代价高,应尽量通过设计降低对拷贝的需求。可考虑使用不可变设计、显式的克隆方法等技术。
  • 在处理循环引用时,要么利用 deepcopy,要么手动提供类似 memo 的机制。
  • 在设计时应确保诸如文件句柄和线程等外部资源不会被复制。

结论

copy 模块是 Python 中用于对象复制的标准工具。正确理解浅拷贝与深拷贝的差异,并在需要时实现自定义拷贝行为,可以让复制过程安全且可预测。在设计阶段明确是否真的需要复制、哪些内容应被共享,可以避免不必要的缺陷与性能问题。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video