copy模块 — 浅拷贝与深拷贝

Python标准库精讲专题 · 数据结构与算法工具篇 · 掌握深浅拷贝机制

专题:Python标准库精讲系统学习

关键词:Python, 标准库, copy, 浅拷贝, 深拷贝, shallow copy, deep copy, __copy__, __deepcopy__

一、变量赋值与引用

在Python中,变量赋值本质上是将一个名称绑定到一个对象上,并不会复制对象本身。理解这一机制是掌握深浅拷贝的前提。

1.1 对象引用机制

Python中的变量可以看作"标签"或"引用",而非存储值的容器。当执行 a = [1, 2, 3] 时,实际发生的是:在内存中创建列表对象 [1, 2, 3],然后将名称 a 指向该对象。此时若执行 b = a,并不会创建新列表,而是让 b 也指向同一个列表对象。

import copy # 变量赋值的本质:多个名称指向同一对象 original = [1, 2, [3, 4]] assigned = original # 只是给同一个对象贴了新标签 print(assigned is original) # True — 完全同一个对象 # 修改"副本"会同时影响"原对象" assigned[0] = 99 print(original[0]) # 99 — 原对象也被修改

1.2 可变对象与不可变对象

理解可变性对拷贝行为至关重要:

# 不可变对象:修改操作会创建新对象 x = 42 y = x # y 指向 42 x = 100 # 创建新对象 100,x 重新指向 print(y) # 42 — y 仍指向原来的 42,不受影响 # 可变对象:修改操作影响所有引用 list_a = [1, 2] list_b = list_a list_a.append(3) # 原地修改 print(list_b) # [1, 2, 3] — list_b 也看到了变化

关键理解:"="赋值永远不创建新对象,它只创建新的引用绑定。如果需要真正独立的副本,必须使用显式拷贝(浅拷贝或深拷贝)。

二、浅拷贝(Shallow Copy)

浅拷贝创建一个新容器对象,然后将原容器中元素的引用填充到新容器中。新容器本身是独立的对象,但其内部的子对象仍然是共享的。

2.1 copy.copy() 语法与基本用法

copy.copy(x) 返回对象 x 的浅拷贝。该函数根据 x 的类型自动选择合适的拷贝方式。

import copy # 浅拷贝:新容器独立,但元素引用共享 nested_list = [[1, 2], [3, 4]] shallow = copy.copy(nested_list) print(shallow is nested_list) # False — 外层是新对象 print(shallow[0] is nested_list[0]) # True — 内层列表仍共享 # 修改外层结构不影响原对象 shallow.append([5, 6]) print(len(nested_list)) # 2 — 原对象不受影响 # 修改内层可变对象会相互影响 shallow[0].append(99) print(nested_list[0]) # [1, 2, 99] — 原对象也被修改了!

2.2 常见的浅拷贝方式

Python中许多操作都会产生浅拷贝,并不局限于 copy.copy()

# 方式一:使用 copy.copy() import copy list1 = [[1, 2], 3] c1 = copy.copy(list1) # 方式二:使用切片 [:] c2 = list1[:] # 方式三:使用 list() 构造器 c3 = list(list1) # 方式四:使用 dict.copy() 方法 d = {'a': [1, 2], 'b': 3} c4 = d.copy() # 方式五:使用 set 拷贝 s = {1, 2, 3} c5 = s.copy() # 方式六:列表推导式也会产生浅拷贝 c6 = [x for x in list1]

2.3 图示说明:浅拷贝的内存布局

以下代码直观展示浅拷贝的内存共享情况:

# 用 id() 追踪对象的身份标识 original = ['A', ['B', 'C'], {'key': 'value'}] shallow = copy.copy(original) print("outer id same?", id(original) == id(shallow)) # False print("inner [0] id same?", id(original[0]) == id(shallow[0])) # True print("inner [1] id same?", id(original[1]) == id(shallow[1])) # True # 可视化内存结构 print("原始对象内存路径: original → 0x{:x}".format(id(original))) print("浅拷贝对象内存路径: shallow → 0x{:x}".format(id(shallow))) print("内部列表共享路径: both → 0x{:x}".format(id(original[1])))

关键记忆:浅拷贝 = 新瓶子装旧酒。新容器是独立的,但容器里的"内容"(元素引用)和原来的完全一样。修改内部可变对象会"隔山打牛"。

三、深拷贝(Deep Copy)

深拷贝创建一个新容器对象,并递归地复制原容器中所有嵌套的可变对象,生成完全独立的副本树。新对象与原对象在内存中没有任何共享的可变子对象。

3.1 copy.deepcopy() 语法

copy.deepcopy(x[, memo]) 返回对象 x 的深拷贝。可选参数 memo 是一个字典,用于跟踪已拷贝的对象(主要用于处理循环引用和自定义缓存)。

import copy # 深拷贝:完全递归复制,生成完全独立的对象树 nested_list = [[1, 2], [3, 4], {'a': [5, 6]}] deep = copy.deepcopy(nested_list) print(deep is nested_list) # False — 外层新对象 print(deep[0] is nested_list[0]) # False — 内层也是新对象! print(deep[2]['a'] is nested_list[2]['a']) # False — 所有层级都独立 # 修改深拷贝中的任何层级都不影响原对象 deep[0].append(99) deep[2]['a'].append(77) print(nested_list[0]) # [1, 2] — 不受影响 print(nested_list[2]['a']) # [5, 6] — 不受影响

3.2 浅拷贝与深拷贝对比

下面通过表格和示例直观对比两者的区别:

对比维度浅拷贝 (Shallow Copy)深拷贝 (Deep Copy)
外层对象创建新对象创建新对象
内层可变对象共享引用递归复制为新对象
不可变子对象共享引用共享引用(不可变对象无需复制)
内存开销高(递归复制整个对象树)
执行速度慢(需遍历所有层级)
独立性部分独立(内层可变对象仍共享)完全独立
# 对比演示 import copy data = {'user': ['Alice', 'Bob'], 'scores': {'math': [90, 85]}} s = copy.copy(data) # 浅拷贝 d = copy.deepcopy(data) # 深拷贝 # 修改内层列表 data['user'].append('Charlie') data['scores']['math'].append(95) print("原对象: ", data) # user 有 3 人,math 有 3 个分数 print("浅拷贝副本: ", s) # user 也有 3 人!math 也有 3 个分数! print("深拷贝副本: ", d) # user 只有 2 人,math 只有 2 个分数 # 输出: # 原对象: {'user': ['Alice', 'Bob', 'Charlie'], 'scores': {'math': [90, 85, 95]}} # 浅拷贝副本: {'user': ['Alice', 'Bob', 'Charlie'], 'scores': {'math': [90, 85, 95]}} # 深拷贝副本: {'user': ['Alice', 'Bob'], 'scores': {'math': [90, 85]}}

四、自定义拷贝行为

对于自定义类,可以通过实现特殊方法来精确控制深浅拷贝的行为,这对于管理文件句柄、数据库连接、单例模式等资源型对象尤为重要。

4.1 实现 __copy__() 方法

__copy__() 用于定义浅拷贝行为。该方法无参数,应返回对象的浅拷贝。

import copy class DatabaseConnection: def __init__(self, host, port, db_name): self.host = host self.port = port self.db_name = db_name self.connection = None # 模拟真实连接,不应被复制 self.query_cache = {} # 查询缓存,浅拷贝时可共享 def __copy__(self): # 浅拷贝:共享连接和缓存,只复制配置信息 new = DatabaseConnection(self.host, self.port, self.db_name) new.query_cache = self.query_cache # 共享缓存 return new def __repr__(self): return f"DB({self.host}:{self.port}/{self.db_name})"

4.2 实现 __deepcopy__() 方法

__deepcopy__(self, memo) 用于定义深拷贝行为。memo 参数是内部使用的字典,用于跟踪已拷贝的对象,自定义方法中需要使用 copy.deepcopy() 复制子对象并传递 memo

import copy class SecureDocument: def __init__(self, content, metadata): self.content = content # 文档内容,需要完整复制 self.metadata = metadata # 元数据字典 self._lock = False # 锁定状态,不应复制 def __deepcopy__(self, memo): # 创建新实例,但跳过 _lock 状态 new = SecureDocument( content=copy.deepcopy(self.content, memo), metadata=copy.deepcopy(self.metadata, memo) ) # _lock 不复制 —— 新文档初始为未锁定状态 return new def lock(self): self._lock = True def is_locked(self): return self._lock

4.3 使用 __getstate__ / __setstate__ 控制拷贝

对于更复杂的序列化控制,可以组合使用 __getstate____setstate__。这两个方法在 pickle 序列化中使用,也被 deepcopy 内部机制调用(对于未显式定义 __deepcopy__ 的对象)。

class FileManager: def __init__(self, filename, mode): self.filename = filename self.mode = mode self.file = open(filename, mode) # 真实文件句柄 def __getstate__(self): # 控制哪些状态会被拷贝/序列化 state = self.__dict__.copy() state['file'] = None # 排除文件句柄 return state def __setstate__(self, state): # 恢复状态时重新打开文件 self.__dict__.update(state) self.file = open(self.filename, self.mode) def __del__(self): if hasattr(self, 'file') and self.file: self.file.close()

最佳实践:包含资源句柄(文件、网络连接、锁)的对象应始终自定义拷贝行为,避免多个副本共享同一资源导致竞态条件。通常在浅拷贝中共享只读资源,在深拷贝中跳过不可序列化的字段。

五、循环引用处理

深拷贝面临的一个特殊问题是循环引用(Circular Reference)——对象直接或间接引用了自身。如果不加处理,递归复制将陷入无限循环。

5.1 deepcopy 的处理机制

copy.deepcopy() 内部使用 memo 字典来跟踪已拷贝的对象。当遇到一个已经拷贝过的对象时,直接返回 memo 中记录的副本,从而打破循环。

import copy # 构造循环引用:A → B → A a = [1, 2] b = [a, 3] # b[0] → a a.append(b) # a[2] → b,形成循环 # a 的结构: [1, 2, [指向 b 的引用]] # b 的结构: [[指向 a 的引用], 3] # 浅拷贝会保留循环引用 shallow = copy.copy(a) print(shallow[2] is a[2]) # True — 内部引用关系不变 # 深拷贝也能正确处理循环引用,不会无限递归 deep = copy.deepcopy(a) print(deep[2] is a[2]) # False — 独立副本 print(deep[2][0] is deep) # True — 循环引用在新副本中正确保持 # 验证深拷贝中循环引用的完整性 print(deep) # [1, 2, [...]] — 正常显示循环 print(deep[2][0][0]) # 1 — 可通过深拷贝中的循环引用访问元素

5.2 memo 字典的运作原理

以下代码模拟了 deepcopy 内部使用 memo 处理循环引用的核心逻辑:

# deepcopy 内部原理示意(简化版本) def _deepcopy_simple(obj, memo=None): if memo is None: memo = {} # 如果对象已经拷贝过,直接返回已有副本 obj_id = id(obj) if obj_id in memo: return memo[obj_id] # 对于不可变对象,直接返回(无需复制) if isinstance(obj, (int, float, str, bytes)): return obj # 创建空副本并注册到 memo(关键:在递归前注册) if isinstance(obj, list): new_obj = [] memo[obj_id] = new_obj # 先注册再递归 for item in obj: new_obj.append(_deepcopy_simple(item, memo)) return new_obj # ... 其他类型的处理 ... raise TypeError(f"Unsupported type: {type(obj)}")

关键机制:deepcopy 在创建空副本后、递归子对象前,就先将"id → 副本"的映射存入 memo。这样当递归路径中再次遇到同一对象时,可以直接返回已创建的副本,优雅地打破循环。这正是 memo 参数存在的意义。

六、性能与注意事项

深浅拷贝的选择直接影响程序性能和内存使用。本节梳理关键性能考量、优化技巧以及常见陷阱。

6.1 深拷贝的性能开销

深拷贝需要递归遍历整个对象树,对于层级深、规模大的数据结构,其时间和空间开销不可忽视:

import copy import time # 性能对比:浅拷贝 vs 深拷贝 large_dict = {'key_{}'.format(i): [0] * 1000 for i in range(1000)} start = time.perf_counter() s = copy.copy(large_dict) print(f"浅拷贝耗时: {time.perf_counter() - start:.6f}秒") start = time.perf_counter() d = copy.deepcopy(large_dict) print(f"深拷贝耗时: {time.perf_counter() - start:.6f}秒") # 输出示例(实际数值因机器而异): # 浅拷贝耗时: 0.000100秒 # 深拷贝耗时: 0.150000秒(慢 1000+ 倍)

6.2 不可变对象的优化

deepcopy 对不可变对象进行了优化——因为它知道不可变对象的内容永远不会变化,所以直接复用原对象引用,避免不必要的复制:

import copy # 不可变对象在深拷贝中不会被真正复制 tup = (1, 2, 3) tup_copy = copy.deepcopy(tup) print(tup_copy is tup) # True — 直接复用 # 但如果元组中包含可变对象,可变部分仍会被递归复制 nested_tup = ([1, 2], 3) deep_tup = copy.deepcopy(nested_tup) print(deep_tup is nested_tup) # False — 外层元组含可变元素,需复制 print(deep_tup[0] is nested_tup[0]) # False — 内层列表被递归复制 print(deep_tup[1] is nested_tup[1]) # True — 整数不可变,直接复用

6.3 copyreg 模块:注册自定义拷贝函数

copyreg 模块允许为特定类型注册自定义的拷贝函数,这对于无法直接修改源码的第三方类型非常有用:

import copy import copyreg import numpy as np # 仅作示例 # 假设有一个第三方类的实例化方式复杂 class ExternalConfig: def __init__(self, data): self.data = data self.cache = {} # 不想被复制的缓存 # 定义自定义的拷贝函数 def _copy_external_config(obj): return ExternalConfig(copy.deepcopy(obj.data)) # 注册到 copyreg —— 自动被 copy.copy() 和 copy.deepcopy() 使用 copyreg.pickle(ExternalConfig, _copy_external_config) # 现在对该类型的拷贝将使用自定义函数 cfg = ExternalConfig({'key': [1, 2, 3]}) cfg_copy = copy.deepcopy(cfg) print(cfg_copy.data is cfg.data) # False — 自定义函数做了深拷贝 print(cfg_copy.cache is cfg.cache) # False — 缓存也被正确处理

6.4 常见陷阱与最佳实践

七、核心要点总结

1. 三种"拷贝"方式的本质区别:

赋值引用(=):不创建新对象,仅仅绑定新名称

浅拷贝(copy.copy):创建新容器,但共享内部可变对象的引用

深拷贝(copy.deepcopy):递归复制所有可变对象,生成完全独立的副本树

2. 浅拷贝的多种形式:

list[:] , list() , dict.copy() , set.copy() , 列表推导式等均为浅拷贝

3. 自定义拷贝接口:

__copy__() 自定义浅拷贝行为

__deepcopy__(self, memo) 自定义深拷贝行为,需注意传递 memo 参数

__getstate__ / __setstate__ 组合控制序列化与拷贝状态

4. 循环引用:

deepcopy 通过 memo 字典跟踪已拷贝对象,先注册空副本再递归子对象,优雅解决循环引用问题

5. 性能优化:

深拷贝比浅拷贝慢数百到数千倍,仅在真正需要完全独立性时使用

不可变对象在深拷贝中自动被优化为引用共享

使用 copyreg 可为第三方类型注册高效的拷贝策略

6. 选择指南:

仅读取,不修改 → 赋值引用(零开销)

只修改容器外层,不修改内部元素 → 浅拷贝(高效安全)

需要完全独立的对象树,或对象结构未知 → 深拷贝(安全可靠)

包含资源句柄、单例、缓存等特殊对象 → 自定义拷贝行为

7. 一句话口诀:

赋值是贴标签,浅拷贝换新瓶装旧酒,深拷贝连瓶带酒全换新。