Consider the following code which uses a WeakSet
at the same time as a finalizer:
>>> 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)}")
Consider the following example:
>>> a = A()
>>> p = Parent(a)
>>> ws.add(p)
>>> weakref.finalize(p, finalizer, a)
<finalize object at 0x1c2ca0d7060; for 'Parent' at 0x1c2ca0aff40>
>>> 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
When swapping the creation of the finalizer and the addition to the WeakSet
, on the other hand:
>>> a = A()
>>> p = Parent(a)
>>> weakref.finalize(p, finalizer, a)
<finalize object at 0x1c2ca0d7060; for 'Parent' at 0x1c2ca0aff40>
>>> 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
Why are these results different? Is there a way to get a consistent value for len(ws)
as the object is being finalized?
The documentation contains the reason why this is happening, in a roundabout way.
In weakref.finalize (emphasis mine):
When the program exits, each remaining live finalizer is called unless its atexit attribute has been set to false. They are called in reverse order of creation.
The hidden consequence of that lies in the fact that calling WeakSet.add
will register a finalizer under the hood. Consequently, in the first case, our own finalizer will be called before the WeakSet
is updated in the reverse order of creation ; in the second case, the WeakSet
is updated before the finalizer.