如何在对象终结阶段获取WeakSet的一致长度?
WeakSet与终结器顺序导致的GC行为差异及解决办法
先看问题中的代码和现象:
基础代码:
import weakref import gc class A: pass class Parent: def __init__(self, a): self.a = a ws = weakref.WeakSet() def finalizer(a): print(f"Finalizing {a}") print(f"Contents of the WeakSet: {ws}") print(f"List of elements in the WeakSet: {list(ws)}") print(f"Length of the WeakSet: {len(ws)}")
现象1:先加入WeakSet,再注册终结器
a = A() p = Parent(a) ws.add(p) weakref.finalize(p, finalizer, a) del p gc.collect()
输出:
Finalizing <__main__.A object at 0x000001C2CA239310> Contents of the WeakSet: {<weakref at 0x000001C2CA270090; dead>} List of elements in the WeakSet: [] Length of the WeakSet: 1
现象2:先注册终结器,再加入WeakSet
a = A() p = Parent(a) weakref.finalize(p, finalizer, a) ws.add(p) del p gc.collect()
输出:
Finalizing <__main__.A object at 0x000001C2CA519370> Contents of the WeakSet: set() List of elements in the WeakSet: [] Length of the WeakSet: 0
为什么结果不同?
核心原因是CPython中弱引用回调的执行顺序规则:
WeakSet给每个加入的对象创建弱引用时,会注册一个回调——当对象被GC回收时,自动从集合里移除失效的引用。而weakref.finalize也会为目标对象注册一个回调,在对象销毁时执行你写的终结函数。
CPython对多个弱引用回调的执行顺序是后注册的回调先执行:
- 现象1中,WeakSet的清理回调先注册,终结器回调后注册。GC触发时,终结器先跑,此时WeakSet还没来得及清理那个已经dead的引用,所以
len(ws)统计的是内部存储的所有引用数(包括失效的),但遍历WeakSet时会自动跳过失效引用,导致len(ws)和list(ws)结果不一致。 - 现象2中,终结器回调先注册,WeakSet的清理回调后注册。GC时,WeakSet的清理回调先执行,把失效引用彻底移除,之后终结器才运行,此时集合已经是空的,所以所有输出结果一致。
另外要注意:WeakSet的__len__方法是直接统计内部容器的元素总数,不管引用是否有效;而它的迭代器会自动过滤失效引用,这才会出现长度和实际存活元素数量不符的情况。
如何在终结阶段获取一致的长度?
有三种可行方案:
- 用
len(list(ws))代替len(ws):转成列表时会自动过滤所有失效引用,得到的是当前存活元素的真实数量,不管回调顺序如何,结果都一致。 - 固定注册顺序:始终先创建终结器,再把对象加入WeakSet。这样GC时WeakSet的清理会先完成,终结器运行时集合状态已经稳定,
len(ws)和遍历结果自然一致。 - 手动触发清理(不推荐):WeakSet内部有个
_commit_removals()方法可以手动清理失效引用,但这是CPython的私有实现细节,不同版本可能变动,不建议在生产代码中使用。
内容的提问来源于stack exchange,提问作者F.X.
相关产品推荐
相关产品推荐

