Python 中 __del__ 未被调用的根源:异常帧对象隐式持有对象引用
当使用 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)
# 输出类似: []
del del_test
# 此时 __del__ 仍不执行,因为 frame 仍在引用它
assert flags[0] == 0 # ✅ 断言通过,证明 __del__ 未触发
? 关键洞察:del_test.run() 内部抛出 TimeoutError 后,run 函数的执行帧(frame)将 del_test 实例保留在其局部命名空间中(例如用于构建 traceback 或异常上下文),直到该帧退出。而由于测试函数本身未结束,该帧持续存活,引用便一直存在。
解决方案:主动清理帧引用
最可靠的方式是显式清空异常上下文并强制 GC 回收:
Chromox
Chromox是一款领先的AI在线生成平台,专为喜欢AI生成技术的爱好者制作的多种图像、视频生成方式的内容型工具平台。
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 资源管理代码的关键一步。
如非特殊说明,本站内容均来自于网友自主分享,如有任何问题均请联系我们进行处理!
猜您喜欢
《锈湖根源》树干部分关卡1图文攻略
2026-06-23锈湖根源树干部分关卡1解法,锈湖,1860年春,詹姆斯范德布姆到达锈湖,他的叔叔刚刚离奇死亡,詹姆斯是它(指房子及其它的遗产唯一的继承人。他完全不知道这些事情将会怎样改变他的人生。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡1图文攻略: ◆进入场景后点击詹姆斯,点击胸前的纸条...
《锈湖根源》树干部分关卡2房子攻略
2026-06-23锈湖根源树干部分关卡2解谜流程,点击房子旁边的黑影开始游戏,进入场景后,把木凳移到詹姆的脚下,点击让他站在木凳上面,点击花盆处,拾取左边的[虫子],点击左边的花盆,拾取[钻石]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡2攻略: 点击房子旁边的黑影开始游戏 ◆进入场景后,把...
《锈湖根源》树干部分关卡3叔叔图文步骤
2026-06-23锈湖根源树干部分关卡3图文攻略,进入场景后点击墙上的相框,点击右上角撕下照片,拾取上面的[钥匙],回退场景后右滑,点击最右边的木柜,打开第二个抽屉,拾取[火柴],用[钥匙]打开第四个抽屉,拾取[斧头]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡3叔叔图文步骤: 点击房子的窗...
《锈湖根源》树干部分关卡4寻找爱图文攻略
2026-06-23锈湖根源树干部分关卡4怎么过,点击下方中间的两个黑影处开始游戏,点击詹姆斯手上的信纸,把[羽毛]给他,用[带血的手帕]把血蹭到信纸上,点击右手蘸血,点击信纸写字“Willyoumarryme?”,写完后点击信纸,拾取[情书]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡4寻找...
《锈湖根源》关卡12搜索解谜攻略
2026-06-23锈湖根源关卡12怎么过,打开柜子后,拾取[图章],回退场景后右滑,点击右边的桌子,将[墨水]倒进左上角的碗中,将[图章]浸到墨水里,得到[有油墨的图章],然后点击信封盖章,盖章后不断点击信封卷起,放入左边的管子中,拾取[管子]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡12搜索解谜...
《锈湖根源》关卡16星星解谜攻略
2026-06-23锈湖根源关卡16图文攻略,本篇主要带来的是艾玛以及她的儿子弗兰克的故事,其中还包括艾尔伯特,点击房子的二楼窗户开始游戏,进入场景后点击左边的桌子,拾取[鸟食]和[卡]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡16星星解谜攻略: 点击房子的二楼窗户开始游戏 ◆进入场景后点击左边的桌...
《锈湖根源》关卡13画图文攻略
2026-06-23锈湖根源关卡13解谜解说,本篇主要带来的是艾玛以及她的儿子弗兰克的故事,其中还包括艾尔伯特,点击树旁边的黑影开始游戏,这一关卡后玩家可以获得第3个牺牲:艾玛(女儿)的眼泪。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡13画图文攻略: 点击树旁边的黑影开始游戏 ◆进入场景后点击左下角的...
《锈湖根源》关卡14井第一部分攻略
2026-06-23锈湖根源关卡14井解谜流程,本篇主要带来的是艾玛以及她的儿子弗兰克的故事,其中还包括艾尔伯特的部分,进入场景后点击泰迪熊,拾取[锐石]和[线],回退场景后点击下水道口,再次点击后,出现一只手,用[锐石]刺向手,拾取[浆果]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡14井第一部分攻...
《锈湖根源》树干部分关卡9孩子的游戏
2026-06-23锈湖根源树干部分关卡9图文攻略,点击下方中间的三个黑影处开始游戏,进入场景后点击右边的树,拾取[蜂蜜][树枝]。把[蜂蜜]放进艾玛抱着的瓶子里,并多次点击艾玛的头带,拾取[松紧带]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡9孩子的游戏: 点击下方中间的三个黑影处开始游戏 ...
《锈湖根源》关卡10生育能力
2026-06-23锈湖根源关卡10解谜流程,当詹姆斯·范德布姆在自己所继承房子的花园里种下一颗特殊的种子后,他的生活发生了剧变。在人生之树上通过揭开画像来扩展你的血缘关系。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡10生育能力: 点击树旁边的黑影开始游戏 ◆进入场景后右滑,拾取[耙子]。 耙子的位置...
