OK, time's up...
There are two things going on here - one's useful to remember for future reference, one's a bit more obscure.
The first thing to note is something about
__del__(), which many people think of as Python's "destructor" as opposed to the
__init__() "constructor". However, if you're used to a language like C++ then it may come as a surprise to learn that
__del__() is called whenever the object is garbage collected, even if it wasn't fully constructed yet. This means that really
__del__() is more like the opposite of
__init__(), and it also means you have to be quite careful about what you do in it - you can't really make any assumptions about the state of the object when it's called, and that makes it rather less useful for general cleanup operations than you might think (in general it's better practice to use the context manager protocol if possible).
So, what's happening above is that
__init__() is called a non-existent method on the
list that's passed in and throwing an
AttributeError as a result. This is fine. Normally, since the base class
__new__() had already returned, we'd then expect
__del__() to also be called, but it wasn't - that's odd. Here's the second trick... Because we didn't catch the
AttributeError exception, the traceback associated with it still exists to supply
sys.last_traceback and one of the stack frames associated with this still contains a reference to the created (but not fully initialised)
You can see this in the transcript below where I first wrote some code to trawl through all the objects in the system (using
gc.get_objects()) and find out what refers to any extant
DictWrapper instances (with
gc.get_referrers()), and then I show the same stack frame can be reached from
>>> import dictwrapper
>>> import gc
>>> import sys
>>> import types
>>> x = dictwrapper.DictWrapper(())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "dictwrapper.py", line 5, in __init__
self.value = init_dict.copy()
AttributeError: 'tuple' object has no attribute 'copy'
>>> for obj in gc.get_objects():
... if isinstance(obj, dictwrapper.DictWrapper):
... for i in gc.get_referrers(obj):
... if isinstance(i, types.FrameType):
... print repr(i)
<frame object at 0xdd99f0>
<frame object at 0xdd99f0>
So, while this stack frame still exists, the instance doesn't get garbage collected and
__del__() isn't called. If we'd have constructed the
DictWrapper instance inside a
try..except block which caught the
AttributeError we would have seen the
__del__() method called immediately.
The final piece to the puzzle comes when executing
print instance. Since
__init__() threw an exception before completing, this exception propogated to the outermost scope and prevented the local
instance variable from being set. This means when
print instance is executed, another
AttributeError is thrown, this time for
instance in the local scope. During this process,
sys.last_traceback is replaced by the current traceback object, since only the most recent exception's information is tracked. This means that the old stack frame is garbage collected and since this is the only remaining reference to the
DictWrapper instance then this is collected too, causing its
__del__() method to finally be called.
__del__() refers to
self.value which was never actually set (since
__init__() threw an exception first) then yet another
AttributeError is thrown. Because this is in a
__del__() method, however, Python ignores exceptions thrown and instead just prints an error message to
stderr, which we can see displayed above just prior to the backtrace for the
instance (which is totally unrelated).
There you go, clear as mud. Again, if anybody wants to clarify anything I'm more than happy to explain further.
Told you it was quite tricky!