10

I was reading about different ways to clean up objects in Python, and I have stumbled upon these questions (1, 2) which basically say that cleaning up using __del__() is unreliable and the following code should be avoid:

def __init__(self):
    rc.open()

def __del__(self):
    rc.close()

The problem is, I'm using exactly this code, and I can't reproduce any of the issues cited in the questions above. As far as my knowledge goes, I can't go for the alternative with with statement, since I provide a Python module for a closed-source software (testIDEA, anyone?) This software will create instances of particular classes and dispose of them, these instances have to be ready to provide services in between. The only alternative to __del__() that I see is to manually call open() and close() as needed, which I assume will be quite bug-prone.

I understand that when I'll close the interpreter, there's no guarantee that my objects will be destroyed correctly (and it doesn't bother me much, heck, even Python authors decided it was OK). Apart from that, am I playing with fire by using __del__() for cleanup?

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Dmitry Grigoryev
  • 3,156
  • 1
  • 25
  • 53
  • Is `__exit__` even guaranteed to execute when the interpreter is closed...? – Rick Feb 18 '16 at 17:48
  • Also: I've found [this link](http://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python) to be very helpful on this subject. – Rick Feb 18 '16 at 17:54
  • @RickTeachey did you mistake `__del__` for `__exit__`, or do you suggest I use `__exit__` in my code? Also, I can tolerate issues when closing. – Dmitry Grigoryev Feb 18 '16 at 17:55
  • 1
    No, I just mean that, since you said "I understand that when I'll close the interpreter, there's no guarantee that my objects will be destroyed correctly..." it seems to imply that `__exit__` will execute in that situation. I'm tempted to say it won't but realized I don't actually know the answer. Sorry- it was more of a side comment. – Rick Feb 18 '16 at 17:57
  • @RickTeachey To be honest, I did imply that without really thinking about it. You got me hooked up, now I'm interested too. – Dmitry Grigoryev Feb 18 '16 at 18:00
  • How important is this `__del__`, if you're okay with it never happening? (I think you may be misunderstanding what the Python developers decided was "okay". They decided it was okay for `__del__` to never run, but the consequence is that things that have to run shouldn't be in `__del__`, not that it's okay to leave critical resources dangling.) – user2357112 Feb 18 '16 at 18:04
  • 1
    @DmitryGrigoryev The answer [seems to be NO](https://dl.dropboxusercontent.com/u/8182793/Stackoverflow/test__exit__.py). – Rick Feb 18 '16 at 18:08
  • @user2357112 I'm OK with anything **at interpreter termination**, even a reboot. This is testing project, meaning that a team of 2 people writes code to test features written by a team of 10, nobody expect us to write perfect code. What I **do** care about is that my code doesn't hang up or lock resources (like COM objects) in the process. – Dmitry Grigoryev Feb 18 '16 at 21:20
  • @user2357112 which reminds me: in another project we already had a [digital signal generator/analyzer](http://www.seskion.de/wwwpub/produkte/psi5/simulyzer.php) which seems to have thread-unsafe API and deadlocks in `DllMain()` when called with `DLL_THREAD_DETACH`. With limited time we had and limited abilities to debug that closed-source DLL, we actually decided we could live with killing the GUI via task manager at the end. – Dmitry Grigoryev Feb 18 '16 at 21:43

3 Answers3

9

You observe the typical issue with finalizers in garbage collected languages. Java has it, C# has it, and they all provide a scope based cleanup method like the Python with keyword to deal with it.

The main issue is, that the garbage collector is responsible for cleaning up and destroying objects. In C++ an object gets destroyed when it goes out of scope, so you can use RAII and have well defined semantics. In Python the object goes out of scope and lives on as long as the GC likes. Depending on your Python implementation this may be different. CPython with its refcounting based GC is rather benign (so you rarely see issues), while PyPy, IronPython and Jython might keep an object alive for a very long time.

For example:

def bad_code(filename):
    return open(filename, 'r').read()

for i in xrange(10000):
    bad_code('some_file.txt')

bad_code leaks a file handle. In CPython it doesn't matter. The refcount drops to zero and it is deleted right away. In PyPy or IronPython you might get IOErrors or similar issues, as you exhaust all available file descriptors (up to ulimit on Unix or 509 handles on Windows).

Scope based cleanup with a context manager and with is preferable if you need to guarantee cleanup. You know exactly when your objects will be finalized. But sometimes you cannot enforce this kind of scoped cleanup easily. Thats when you might use __del__, atexit or similar constructs to do a best effort at cleaning up. It is not reliable but better than nothing.

You can either burden your users with explicit cleanup or enforcing explicit scopes or you can take the gamble with __del__ and see some oddities now and then (especially interpreter shutdown).

schlenk
  • 7,002
  • 1
  • 25
  • 29
  • Thanks. I actually rely on more than best-effort cleanup in my code. Could you clarify what does CPython guarantee in your example? That there's at most one open handle? At most 10? Something else? – Dmitry Grigoryev Feb 18 '16 at 19:04
  • 2
    CPython uses a reference counting system to handle cleanup. So in the example above the handle goes out of scope, which reduces the refcount and is cleaned up right away, so only a single handle is open while the function runs. – schlenk Feb 18 '16 at 20:49
  • That's good enough for me. I can get away with a requirement for specific Python interpreter to run my code. – Dmitry Grigoryev Feb 18 '16 at 21:30
2

There are a few problems with using __del__ to run code.

For one, it only works if you're actively keeping track of references, and even then, there's no guarantee that it will be run immediately unless you're manually kicking off garbage collections throughout your code. I don't know about you, but automatic garbage collection has pretty much spoiled me in terms of accurately keeping track of references. And even if you are super diligent in your code, you're also relying on other users that use your code being just as diligent when it comes to reference counts.

Two, there are lots of instances where __del__ is never going to run. Was there an exception while objects were being initialized and created? Did the interpreter exit? Is there a circular reference somewhere? Yep, lots that could go wrong here and very few ways to cleanly and consistently deal with it.

Three, even if it does run, it won't raise exceptions, so you can't handle exceptions from them like you can with other code. It's also nearly impossible to guarantee that the __del__ methods from various objects will run in any particular order. So the most common use case for destructors - cleaning up and deleting a bunch of objects - is kind of pointless and unlikely to go as planned.

If you actually want code to run, there are much better mechanisms -- context managers, signals/slots, events, etc.

Brendan Abel
  • 35,343
  • 14
  • 88
  • 118
  • Could you elaborate about exceptions in __init__? Should I catch and rethrow? I know 'with' is the way to go, but I have to provide an object to a closed source caller, so my options seem limited – Dmitry Grigoryev Feb 18 '16 at 19:21
  • If you're creating or doing things in your `__init__` and there is an error during the `__init__`, it won't complete, and a reference to that class will never be created, so the `__del__` will never get called to tear down or cleanup whatever you created before the error occurred. What are the specs for the object you have to provide? Can you point me to the docs for the closed source library? – Brendan Abel Feb 18 '16 at 19:26
  • There's [this page](http://www.isystem.com/downloads/testIDEA/help/) which has some info if you'd like to take a look. The relevant section is Tasks->Writing Script Extensions. – Dmitry Grigoryev Feb 18 '16 at 21:09
  • 1
    @Brendan Abel An error in `__init__` does not prevent the `__del__` from being called. – wombatonfire Dec 28 '19 at 14:16
  • `__init__` is called on already created objects, so the reference already exist, what could happen is you get `AttributeError` when trying to access not set attributes on your `__del__`. – hldev Dec 19 '22 at 23:21
2

If you're using CPython, then __del__ fires perfectly reliably and predictably as soon as an object's reference count hits zero. The docs at https://docs.python.org/3/c-api/intro.html state:

When an object’s reference count becomes zero, the object is deallocated. If it contains references to other objects, their reference count is decremented. Those other objects may be deallocated in turn, if this decrement makes their reference count become zero, and so on.

You can easily test and see this immediate cleanup happening yourself:

>>> class Foo:
...     def __del__(self):
...         print('Bye bye!')
... 
>>> x = Foo()
>>> x = None
Bye bye!
>>> for i in range(5):
...     print(Foo())
... 
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
<__main__.Foo object at 0x7f037e6a0550>
Bye bye!
>>>

(Though if you want to test stuff involving __del__ at the REPL, be aware that the last evaluated expression's result gets stored as _, which counts as a reference.)

In other words, if your code is strictly going to be run in CPython, relying on __del__ is safe.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459