After discussing my last post with a friend and talking about a few other issues, we came to the conclusion that it would be worthwhile to discuss more gotchas.
First though, what is a gotcha? Wikipedia gives a good definition:
In programming, a gotcha is a feature of a system, a program or a programming language that works in the way it is documented but is counter-intuitive and almost invites mistakes because it is both enticingly easy to invoke and completely unexpected and/or unreasonable in its outcome.
So let’s start with “__del__ is not the opposite of __init__”.
If you come from c++ or a similar background, you are probably well versed in object oriented concepts, specifically, constructors and destructors. The usual expectation is to have the destructor called only for fully constructed objects – i.e., objects whose constructor returned without raising an exception.
If the constructor raises an exception, it is expected to “clean up after itself”, and not expect the destructor to run.
Since in Python __init__ is the de-facto constructor, and __del__ is considered the destructor, most people expect this line of reasoning to work with __init__ and __del__.
This is mistaken. __del__ is not the opposite of __init__, but rather of __new__. Which means that if __init__ raises an exception, then __del__ will still be called.
I’ve run into this issue myself several times in the past. Consider the following sample code:
class A(object): def __init__(self,x): if x == 0: raise Exception() self.x = x def __del__(self): print self.x |
This code demonstrates a common case: a constructor that might fail, and a destructor that does something with the instance’s members. If you try to instantiate A with x = 0, you’ll get an exception. This is to be expected.
However, what is less expected is when the partially constructed A is garbage-collected (which may be anytime later, and not necessarily right away):
Exception exceptions.AttributeError: "'A' object has no attribute 'x'" in <bound method A.__del__ of <__main__.A object at 0x02449570>> ignored |
What happened is that __del__ was called even though __init__ raised an exception. When __del__ tried to access self.x it got an attribute error, because it hasn’t been defined yet.
The solution?
1. Don’t use __del__ unless you really have to. I’m going to write a more about it soon.
2. If you do use __del__ make sure you are covered for any case in which __init__ didn’t finish running.
It is best to either define all variables in __init__ with a default value before doing anything else, or in other methods, use getattr(self, ‘name’) when accessing attributes, to avoid attribute errors.
@nirs:
You really can’t be sure __init__ runs through anything. The exception that caused it to exit in mid-execution could have been KeyboardInterrupt. If you want defaults, they are better defined as class attributes, so that they’re available to any instance; but better yet, is to do what the post says, and avoid __del__.
Class attributes can be really dangerous. If you have, for example, a list as a class attribute and you do changes to this list, it affects all instances of that class.
nice one, it actually makes sense – and important to know :)
Good idea about class attributes here – it can clean up lot of code and even save lot of uninteresting __init__ implementations.
Couldn’t stress enough how much __del__ is unnecessary in most cases.
95% of python OO code I CR has unnecessary and possibly buggy __del__s.
And not to even BEGIN talking about reference cycles and the problem you have with __del__’s in that case.
Anyway – would love if you stress about it in a post :)
It seems to me that the __del__ function must have a purpose and be necessary, otherwise why would the people who maintain and develop python keep the function around.
I encountered this issue today and thanks to this article I resolved my problem. I realize now the proper way to use the __del__ deconstructor would have been to initialize all of the properties that I would be affecting on the first lines in the __init__ constructor.
If I were a better coder and followed the most primitive standard of declaring and initializing variables first and at the top of my code I would’t have seen this issue at all.
Anyway good post and great commentary from everyone ;-)
Where I can – I’ve been using context managers for situations that it is critical to release a hardware lock or similar – because as far as I can see, __del__ is not something you can control the timing of.
Your confusion starts here: “Since in Python __init__ is the de-facto constructor, and __del__ is considered the destructor,…”
__init__() isn’t the class instance constructor; __new__() is. __init__() is the instance initializer.
Thinking of __init__() as the constructor is what gets one into the mess you describe.
https://www.python.org/download/releases/2.2/descrintro/#__new__
why not use
try:
…
except AttributeError:
…
within __del__