del 不执行主因是引用未释放,常见于异常帧持有或循环引用;__del__ 不触发需用 gc.get_referrers() 和 gc.garbage 定位,替代方案应优先选用显式 close() 或上下文管理器。

<strong>del</strong> 方法不执行,绝大多数时候不是你写错了,而是对象还被谁“拽着”——引用计数没降到 0,GC 就不会碰它。Python 不保证 <strong>del</strong> 一定执行,更不保证何时执行;它只在对象确定不可达且无循环引用、且当前帧未隐式持有时,才可能被调用。
为什么 del 后 __del__ 没触发?看引用谁在 holding
执行 del obj 只是删掉一个名字,不等于对象立刻消失。关键要看它的引用计数是否真归零:
- 用
sys.getrefcount(obj)查当前引用数(注意:传参本身会+1,结果要减 1 才准) - 更直接的是
gc.get_referrers(obj)—— 它能列出所有持有该对象引用的对象,比如:[<frame at>]就说明某个栈帧还在引用它 - 常见“拽着不放”的元凶:异常帧(
mock.Mock(side_effect=[Exception])触发后,run()的帧会把self锁在f_locals里)、闭包、全局缓存、日志 handler、线程局部存储
mock 测试中 __del__ 失效:帧引用陷阱
这是测试代码里最隐蔽也最高频的失效场景。只要你在 try/except 块里用 mock.Mock(side_effect=[SomeError]),哪怕异常被捕获了,当前函数帧仍会强引用 self 实例。
- 现象:
del obj后__del__不执行,flags不变,gc.get_referrers(obj)返回一个<frame> - 原因:Python 在异常发生时,会把当前帧的局部变量(含
self)、异常对象、traceback 全部打包进帧对象,直到该帧退出才释放 - 修复方式(Python 3.12+):
import sys try: ... except SomeError: # 清空当前帧的异常上下文 sys.exc_info() # 触发一次,确保有值 # 然后手动断开引用(无副作用) del sys.last_traceback, sys.last_type, sys.last_value # 仅限交互环境 # 更稳妥:显式删除局部变量 locals().pop('obj', None)
循环引用让 __del__ 彻底失能
当 A 持有 B,B 又持有 A,即使外部所有引用都删光,它们的引用计数也卡在 ≥1,GC 的引用计数器根本看不到“该回收了”。此时 __del__ 永远不会被调用。
- 典型结构:
class Node: def __init__(self): self.parent = None; self.children = [],父子互引 - 检测方法:
gc.collect()返回值为 0,但gc.garbage非空 → 说明存在不可达循环引用 - 破环手段:
- 用
weakref.ref替代强引用(如self._parent_ref = weakref.ref(parent)) - 在关键生命周期点(如
close()或__exit__)主动置空反向引用:node.parent = None - 避免在
__init__中自动建立双向链接,改由外部协调
- 用
别依赖 __del__ 做关键清理
它天生不可靠:可能不执行、延迟执行、多线程下竞态、甚至模块卸载后调用引发 ImportError。真正需要确定性资源释放,请换更可控的方式:
- 显式
close()+__enter__/__exit__:数据库连接、文件句柄、线程必须走这条路 - 用
atexit.register()做进程退出兜底(仅限主解释器,不适用于 fork 或多进程) - 对简单对象,直接用
del obj+gc.collect()强制触发(仅调试用,勿进生产) - 永远不要在
__del__里做网络请求、锁操作、或调用可能已卸载模块的函数
真正的难点不在写 <strong>del</strong>,而在确认它有没有被调用、为什么没被调用、以及有没有更稳的替代路径。帧引用和循环引用是两个最常被忽略的底层动因,查的时候优先用 gc.get_referrers() 和 gc.garbage,比猜快得多。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











