Search code examples
pythondictionaryweak-references

Why is value not removed from WeakValueDictionary when last stong reference goes away


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
>>> 
>>> 

Solution

  • 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