__del__会触发循环引用问题,因为父类与子类相互强引用且子类__del__中反向访问父类时,CPython引用计数无法释放二者,形成不可达闭环;应改用weakref或显式生命周期管理来破环。

为什么 __del__ 会触发循环引用问题
当父类持有子类实例的引用,而子类又在 __del__ 中反向访问父类(比如调用父类方法或读取父类属性),且父类本身也强引用着该子类实例时,CPython 的引用计数机制就无法自动回收这两个对象。它们彼此“拽着对方”,形成一个不可达但未被释放的闭环。
常见于事件回调、观察者模式、资源代理类中——比如子类注册到父类的 callbacks 列表里,又在 __del__ 里试图从父类注销自己。
- 只要存在双向强引用 +
__del__,就可能卡住 GC -
gc.collect()通常能强制清理,但不解决根本设计缺陷 - PyPy 或其他解释器行为更不可控,不能依赖手动 gc
用 weakref 打断强引用链
最直接有效的解法是把“子类对父类的反向引用”换成弱引用。这样父类销毁时,子类不会阻止它被回收;子类销毁时,弱引用自动变为 None,也不会拖慢父类释放。
典型写法:在子类中用 weakref.ref(parent) 存父类,使用前先调用返回值检查是否还活着:
立即学习“Python免费学习笔记(深入)”;
import weakref
<p>class Parent:
def <strong>init</strong>(self):
self.children = []</p><p>class Child:
def <strong>init</strong>(self, parent):
self._parent_ref = weakref.ref(parent) # 不增加 parent 引用计数
parent.children.append(self)</p><pre class="brush:php;toolbar:false;">def do_something(self):
parent = self._parent_ref()
if parent is not None: # 确保 parent 还活着
parent.notify_child_action(self)
- 不要在
__del__里调用self._parent_ref()—— 此时父类可能已开始析构,状态不确定 - 避免对
weakref.ref对象做==或is比较,应始终调用它获取目标 -
weakref.finalize可用于注册清理逻辑,比__del__更可靠
绕开 __del__:显式生命周期管理
很多所谓“必须用 __del__ 清理”的场景,其实只是没把资源释放逻辑提前暴露出来。主动调用 .close()、.shutdown() 或上下文管理协议(__enter__/__exit__)才是更可控的方式。
- 把子类注册/注销逻辑拆成两个明确方法,比如
attach_to()和detach() - 在父类
__del__里遍历子类列表并调用其detach(),但前提是子类自身不依赖父类状态 - 用
contextlib.closing或自定义 context manager 封装,确保退出时清理 - 若用 asyncio,优先走
async def __aenter__/__aexit__,避免与事件循环交互时机错乱
验证是否还有循环引用残留
别猜,用工具看。启用 GC 调试后运行关键路径,检查是否有对象长期滞留:
import gc
gc.set_debug(gc.DEBUG_SAVEALL) # 让 gc 把无法回收的对象存进 gc.garbage
# ... 触发你的父子类创建和销毁流程 ...
print(len(gc.garbage)) # 非零说明有泄漏
# 查看具体类型
for obj in gc.garbage[:3]:
print(type(obj), getattr(obj, '__dict__', 'no __dict__'))
- 注意:开启
DEBUG_SAVEALL会影响性能,仅用于调试 - 如果看到
Child和Parent同时出现在gc.garbage里,基本就是循环引用没断干净 -
objgraph.show_backrefs([some_child], max_depth=3)(需安装objgraph)能可视化引用链,比纯日志直观得多
真正难的不是加 weakref,而是识别哪些引用本就不该是强的——尤其在多层嵌套回调或动态插件系统里,一个不经意的 self.parent = parent 就埋下隐患。每次加引用前,先问一句:这个引用是否必须存活到对方销毁之后?


















