deepcopy在循环引用时因memo机制失效或递归过深触发RecursionError;应重写__deepcopy__、用weakref解耦、避免拷贝不可序列化对象。

deepcopy 为什么在循环引用时卡住或报 RecursionError
Python 的 copy.deepcopy() 内部靠 memo 字典缓存已拷贝对象的 id() 来识别并跳过重复递归,但这个机制依赖“对象可哈希且 ID 稳定”。一旦类重写了 __eq__ 或 __hash__,导致 id() 比较失效(比如用弱引用、代理对象或自定义内存管理),memo 就可能漏判循环;更常见的是嵌套层级过深(>1000 层),触发默认的 sys.getrecursionlimit() 限制。
典型错误现象:RecursionError: maximum recursion depth exceeded,或进程长时间无响应(尤其在含大量图结构、双向链表、事件监听器绑定的类实例中)。
- 不要临时调用
sys.setrecursionlimit(3000)—— 这只是掩盖问题,上线环境极易因栈溢出崩溃 - 检查是否无意中让方法闭包捕获了
self(如 lambda 中引用实例属性),这类引用不会出现在obj.__dict__里,但会被deepcopy追踪到 - 确认对象里没混入不可拷贝类型:文件句柄、线程锁、C 扩展对象(如某些数据库连接)会直接抛
TypeError: unshallowable object
手动控制 deepcopy 流程:重写 __deepcopy__ 方法
对自定义类,最稳妥的方式是显式接管深拷贝逻辑,避免盲目递归。重点不是“全拷”,而是“只拷该拷的”。
示例场景:一个带双向父子引用的树节点类:
立即学习“Python免费学习笔记(深入)”;
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
self.parent = None # 循环引用来源
<pre class="brush:php;toolbar:false;">def __deepcopy__(self, memo):
# 创建新实例,跳过 parent(避免循环)
new_obj = TreeNode(self.value)
memo[id(self)] = new_obj
# 只深拷 children,parent 留空,后续由上层逻辑重建关系
new_obj.children = copy.deepcopy(self.children, memo)
return new_obj-
memo必须传给子对象的copy.deepcopy()调用,否则子级递归会重新建memo,破坏循环检测 - 不要在
__deepcopy__里调用copy.deepcopy(self)—— 会无限递归 - 若需保留 parent 关系,应在拷贝完成后由外部统一重建(如遍历新树补 parent 指针),而非在拷贝过程中强求
绕过 deepcopy:用 weakref 或 ID 映射解耦循环
很多所谓“循环引用”本质是设计冗余。与其硬扛深拷贝,不如提前消除强循环。
常见重构方式:
- 把
self.parent改成self._parent_id: int,配合全局 registry 查找,彻底断开引用链 - 用
weakref.ref替代强引用:self._parent_ref = weakref.ref(parent),这样deepcopy不会追踪它(weakref 对象本身不可拷贝,但不会引发循环) - 对图结构,改用邻接表 + 索引 ID,而非对象直连:
self.neighbors = [node.id for node in raw_neighbors],拷贝时只复制 ID 列表,重建时再查表
注意:weakref.ref 在被拷贝对象销毁后会自动变为 None,所以使用前必须加 if ref() is not None 判断。
NumPy 数组、datetime、枚举等特殊对象别乱用 deepcopy
copy.deepcopy() 对标准库外的类型支持有限,强行使用反而出错。
-
numpy.ndarray:必须用a.copy(),deepcopy(a)可能破坏dtype或strides,旧版 NumPy 甚至抛AttributeError: 'numpy.ndarray' object has no attribute '__dict__' -
datetime、enum.Enum、frozenset等不可变对象,deepcopy实际返回原对象(因为无需拷),但代码逻辑若假设“深拷贝一定新建对象”,就会埋下隐性 bug - 含
__slots__的类,deepcopy默认不拷__slots__外字段;若用了描述符或 property,需在__deepcopy__里显式处理
真正难的不是写对一行 copy.deepcopy(x),而是判断 x 里哪些部分本就不该被拷——比如配置里的 logger 实例、全局计数器、缓存池句柄。这些往往比循环引用更容易被忽略。


















