专题: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 可变对象与不可变对象
理解可变性对拷贝行为至关重要:
- 不可变对象(int、float、str、tuple、frozenset):对象创建后内容不可更改,修改操作会创建新对象,因此拷贝时通常共享引用。
- 可变对象(list、dict、set、自定义类实例):对象内容可原地修改,拷贝时需决定是共享内部元素还是递归复制。
# 不可变对象:修改操作会创建新对象
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 常见陷阱与最佳实践
- 陷阱一:误以为
list.copy() / dict.copy() 是深拷贝。这些方法都是浅拷贝,嵌套对象仍共享引用。
- 陷阱二:对单例模式对象使用深拷贝可能破坏单例约束。应在类中实现
__deepcopy__ 返回单例实例。
- 陷阱三:深拷贝包含
__slots__ 的类时,需确保 __slots__ 中的属性都能被正确访问。
- 建议:对于不可变值对象(如 dataclass 配置),优先使用
dataclasses.replace() 替代深拷贝。
- 建议:频繁拷贝大型数据时,考虑使用
__slots__ 减少对象内存,或使用数组模块(array、numpy)替代嵌套列表。
- 建议:如果只需要部分字段的副本,手动构造新对象通常比深拷贝更高效且意图更清晰。
七、核心要点总结
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. 一句话口诀:
赋值是贴标签,浅拷贝换新瓶装旧酒,深拷贝连瓶带酒全换新。