I have the following Python program:
import weakref
class NumberWord:
def __init__(self, word):
self.word = word
def __repr__(self):
return self.word
dict = weakref.WeakValueDictionary()
print(f"[A] {len(dict)}")
print(f"dict.get(0) = {dict.get(0)}")
print(f"dict.get(1) = {dict.get(1)}")
list = []
list.append(NumberWord("zero"))
dict[0] = list[0]
print(f"[B] {len(dict)}")
print(f"dict.get(0) = {dict.get(0)}")
print(f"dict.get(1) = {dict.get(1)}")
list.append(NumberWord("one"))
dict[1] = list[1]
print(list)
print(f"[C] {len(dict)}")
print(f"dict.get(0) = {dict.get(0)}")
print(f"dict.get(1) = {dict.get(1)}")
list.pop()
print(list)
print(f"[D] {len(dict)}")
print(f"dict.get(0) = {dict.get(0)}")
print(f"dict.get(1) = {dict.get(1)}")
list.pop()
print(list)
print(f"[E] {len(dict)}")
print(f"dict.get(0) = {dict.get(0)}")
print(f"dict.get(1) = {dict.get(1)}")
I expect the following behavior:
At step [A] the dict is empty
At step [B] the dict contains dict[0] = NumberWord("zero")
At step [C] the dict contains dict[0] = NumberWord("zero")
and dict[1] = NumberWord("one")
At step [D] the dict contains dict[1] = NumberWord("one")
("zero" is removed because the only strong reference which was in the list went away)
At step [E] the dict is empty again ("one" is removed because the only strong reference which was in the list went away)
Everything works as expected except step [E]: "one" does not go away. Why not?
Here is the actual output:
>>> import weakref
>>>
>>> class NumberWord:
... def __init__(self, word):
... self.word = word
... def __repr__(self):
... return self.word
...
>>> dict = weakref.WeakValueDictionary()
>>>
>>> print(f"[A] {len(dict)}")
[A] 0
>>> print(f"dict.get(0) = {dict.get(0)}")
dict.get(0) = None
>>> print(f"dict.get(1) = {dict.get(1)}")
dict.get(1) = None
>>>
>>> list = []
>>> list.append(NumberWord("zero"))
>>> dict[0] = list[0]
>>>
>>> print(f"[B] {len(dict)}")
[B] 1
>>> print(f"dict.get(0) = {dict.get(0)}")
dict.get(0) = zero
>>> print(f"dict.get(1) = {dict.get(1)}")
dict.get(1) = None
>>>
>>> list.append(NumberWord("one"))
>>> dict[1] = list[1]
>>> print(list)
[zero, one]
>>>
>>> print(f"[C] {len(dict)}")
[C] 2
>>> print(f"dict.get(0) = {dict.get(0)}")
dict.get(0) = zero
>>> print(f"dict.get(1) = {dict.get(1)}")
dict.get(1) = one
>>>
>>> list.pop()
one
>>> print(list)
[zero]
>>>
>>> print(f"[D] {len(dict)}")
[D] 2
>>> print(f"dict.get(0) = {dict.get(0)}")
dict.get(0) = zero
>>> print(f"dict.get(1) = {dict.get(1)}")
dict.get(1) = one
>>>
>>> list.pop()
zero
>>> print(list)
[]
>>>
>>> print(f"[E] {len(dict)}")
[E] 1
>>> print(f"dict.get(0) = {dict.get(0)}")
dict.get(0) = zero
>>> print(f"dict.get(1) = {dict.get(1)}")
dict.get(1) = None
>>>
>>>
I just discovered the answer myself.
The reason is special variable _
still contains the result of the last evaluation.
The last evaluation was list.pop()
and its result was NumberWord("zero")
.
As long as this result is still stored in _
we will continue to have strong reference and the weak reference doesn't go away.
We can confirm this theory by causing another evaluation to happen. At that point _
will contain a different value, and the weak reference will go away:
If we execute the following additional statements at the end of the example above:
_
5 + 5
_
print(f"[F] {len(dict)}")
print(f"dict.get(0) = {dict.get(0)}")
print(f"dict.get(1) = {dict.get(1)}")
Then we get the following output:
>>> _
zero
>>> 5 + 5
10
>>> _
10
>>> print(f"[F] {len(dict)}")
[F] 0
>>> print(f"dict.get(0) = {dict.get(0)}")
dict.get(0) = None
>>> print(f"dict.get(1) = {dict.get(1)}")
dict.get(1) = None