data data - 3 months ago 22
Python Question

strange python destructor behaviour

While playing with OO Python I came across following curiosity. Consider following simple class:

>>> class Monty():
def __init__(self):
print 'start m'
def __del__(self):
print 'deleted m'


Instantiating object goes as expected:

>>> a = Monty()
start m
>>> a
<__main__.Monty instance at 0x7fdf9f084830>


and now the funny part:

>>> del a
>>> a = Monty()
start m
>>> a
deleted m
<__main__.Monty instance at 0x7fdf9f083fc8>
>>> a
<__main__.Monty instance at 0x7fdf9f083fc8>
>>> del a
>>> a = Monty()
start m
>>> del(a)
deleted m
>>> a = Monty()
start m
>>> a
deleted m
<__main__.Monty instance at 0x7fdf9f084830>


The confusing part here is that I get message from destructor with delay. My understanding is that:

del a


Deletes object reference so that object a is left for garbage collection. But for some reason interpreter waits with passing message to console.

Another this is the difference between

del x


and

del(x)


since if you ran the code using the latter only everything goes as expected - you get immediate info from destructor.

It is possible to reproduce this on Python 2.7 as well as using Python 3.3.

Answer

The Python interpreter creates an additional reference. Every time an expression doesn't return None, the result is echoed and stored in the _ built-in name.

When you then echo a different result, _ is rebound to the new result, and the old object reference count drops. In your case, that means that only then is the previous Monty() instance reaped.

In other words, when you execute del a, you are not removing the last reference. Only when you later echo the new object is the last reference gone:

>>> a = Monty()   # reference count 1
start m
>>> a             # _ reference added, count 2
<__main__.Monty instance at 0x7fdf9f084830>    
>>> del a         # reference count down to 1 again
>>> a = Monty()
start m
>>> a             # _ now references the new object, count drops to 0
deleted m         # so the old object is deleted.
<__main__.Monty instance at 0x7fdf9f083fc8>

You can see the reference by echoing _, and you can clear the reference by echoing something entirely unrelated:

>>> a = Monty()   # reference count 1
start m
>>> a             # echoing, so _ is set and count is now 2
<__main__.Monty instance at 0x1056a2bd8>
>>> _             # see, _ is referencing the same object
<__main__.Monty instance at 0x1056a2bd8>
>>> del a         # reference count down to 1
>>> _             # _ is still referencing the result
<__main__.Monty instance at 0x1056a2bd8>
>>> 'some other expression'    # point to something else
deleted m                      # so reference count is down to 0
'some other expression'
>>> _
'some other expression'

There is no difference between del x and del(x). Both execute the del statement, the parentheses are part of the expression and are a no-op in this case. There is no del() function.

The real difference is that you did not echo a in that section of the code, so no additional _ references were created.

You'd get the exact same results wether or not you use (..) parentheses:

>>> a = Monty()  # ref count 1
start m
>>> del a        # no _ echo reference, so count back to 0
deleted m
>>> a = Monty()  # ref count 1
start m
>>> del(a)       # no _ echo reference, so count back to 0
deleted m