当使用 mock.Mock(side_effect=[Exception]) 触发异常时,Python 的当前执行帧(frame)会隐式持有对被测试对象的引用,导致其引用计数无法降为 0,从而阻止 __del__ 方法执行——这是 del 语义与异常处理机制交互产生的常见陷阱。
当使用 `mock.mock(side_effect=[exception])` 触发异常时,python 的当前执行帧(frame)会隐式持有对被测试对象的引用,导致其引用计数无法降为 0,从而阻止 `__del__` 方法执行——这是 `del` 语义与异常处理机制交互产生的常见陷阱。
在 Python 中,__del__ 方法的触发依赖于垃圾回收器(GC)检测到对象的引用计数归零。但这一机制在异常上下文中极易失效:*一旦异常被抛出(即使被 except 捕获),当前栈帧(frame)会自动将局部变量、异常值及关联对象存入 f_locals 和 `fexc属性中,形成隐式强引用**。这正是你观察到test_del_fails中del del_test后del` 未执行的根本原因。
我们可通过 gc.get_referrers() 直观验证该问题:
import gc
import unittest.mock as mock
class DelTest:
def __init__(self, flags, test_fn):
self.flags = flags
self.test_fn = test_fn
def __del__(self):
self.flags[0] = 1
def run(self):
try:
self.test_fn()
except TimeoutError:
pass
def test_del_fails_debug():
flags = [0]
test_fn = mock.Mock(side_effect=[TimeoutError()])
del_test = DelTest(flags, test_fn)
del_test.run()
# 查看谁在引用 del_test
referrers = gc.get_referrers(del_test)
print("Referrers to del_test:", referrers)
# 输出类似: [<frame at 0x... code run>]
del del_test
# 此时 __del__ 仍不执行,因为 frame 仍在引用它
assert flags[0] == 0 # ✅ 断言通过,证明 __del__ 未触发? 关键洞察:del_test.run() 内部抛出 TimeoutError 后,run 函数的执行帧(frame)将 del_test 实例保留在其局部命名空间中(例如用于构建 traceback 或异常上下文),直到该帧退出。而由于测试函数本身未结束,该帧持续存活,引用便一直存在。
解决方案:主动清理帧引用
最可靠的方式是显式清空异常上下文并强制 GC 回收:
立即学习“Python免费学习笔记(深入)”;
import gc
import sys
import unittest.mock as mock
def test_del_fails_fixed():
flags = [0]
test_fn = mock.Mock(side_effect=[TimeoutError()])
del_test = DelTest(flags, test_fn)
del_test.run()
assert flags[0] == 0
# 关键步骤:清除当前帧中的异常状态
sys.exc_clear() # Python 2;Python 3.12+ 已弃用,改用以下方式
# ✅ Python 3 推荐做法:
del sys.last_traceback, sys.last_type, sys.last_value # 若存在
# 更通用的做法:手动删除局部变量 + 强制 GC
del del_test
gc.collect() # 显式触发回收,促使 __del__ 执行
# 等待 GC 完成(__del__ 可能异步执行)
import time
time.sleep(0.001) # 微小延迟确保 __del__ 调用(仅测试环境适用)
assert flags[0] == 1 # ✅ 现在通过但需注意:生产代码中绝不应依赖 __del__ 做关键资源清理(如关闭文件、释放锁)。Python 官方明确指出 __del__ 具有不确定性(可能不被调用、调用时机不可控、甚至引发 RuntimeError)。正确做法是使用上下文管理器(with 语句)或显式 .close() 方法:
class DelTest:
def __init__(self, flags, test_fn):
self.flags = flags
self.test_fn = test_fn
self._closed = False
def close(self):
if not self._closed:
self.flags[0] = 1
self._closed = True
def __enter__(self):
return self
def __exit__(self, *exc_info):
self.close()
# ✅ 推荐用法
def test_with_context():
flags = [0]
test_fn = mock.Mock(side_effect=[TimeoutError()])
with DelTest(flags, test_fn) as dt:
dt.run()
assert flags[0] == 1 # 确保执行总结
- __del__ 不是析构钩子,而是“最终化”(finalizer),行为受 GC 和帧生命周期严格约束;
- 异常发生时,Python 帧会隐式持有活跃对象引用,这是语言设计使然,非 pytest/mock 特有缺陷;
- 测试中若需验证 __del__ 行为,请配合 gc.collect() 和 gc.get_referrers() 进行调试;
- 永远优先选择 contextlib.contextmanager、__enter__/__exit__ 或显式资源管理,而非依赖 __del__。
理解帧引用机制,是写出可预测、可维护 Python 资源管理代码的关键一步。


















