Safely using destructors in Python (2009)
eli.thegreenplace.net
eli.thegreenplace.net
There are a lot of ways to do so comfortably. A trick I really like is to use `contextlib.contextmanager`, and have a pure constructor and a separate static factory method that injects appropriate contexts. This also makes the code more loosely coupled in general :-)
from contextlib import contextmanager
class Session:
@classmethod
@contextmanager
def create(cls):
with db.connect(...) as connection:
yield cls(connection)
def __init__(self, connection: db.Connection):
self._connection = connection
def login(self, name, password):
self._connection.query(...)
with Session.create() as session:
... class Session:
def __init__(self):
self._connection = db.connect(...)
def __enter__(self):
self._connection.__enter__(self)
return self
def __exit__(self, *args):
return self._connection.__exit__(self)
def login(self, name, password):
self._connection.query(...)
with Session() as session:
...
It might need a bit more knowledge about context managers [1], but it feels less magic and more explicit to me – i.e. more pythonic :). But it also contains the assumption that db.connection already returns the connection. If that is not true, a bit more housekeeping is needed: class Session:
def __init__(self):
self._connection_context = db.connect(...)
def __enter__(self):
self._connection = self._connection_context.__enter__(self)
return self
def __exit__(self, *args):
return self._connection_context.__exit__(self)
def login(self, name, password):
self._connection.query(...)
[1] https://docs.python.org/3/library/stdtypes.html#typecontextm...But most importantly, generators as enclosures are used everywhere today with the whole async-await paradigm.
I honestly don't remember ever having to write a __del__ method in the decade I've been using Python.
in some cases resources are long-lived and not possible to enclose in a context manager. as an example, a while ago i wrote some PyOpenGL cide and wanted to automatically release some temp GPU buffers when no longer needed. however, they had to live for multiple iterations of the app's main loop, so a context manager wouldn't work – __del__ was the best place to release them.
Isn't the issue with __del__ that you have no idea when or even if it will ever run? This is always a problem with destructors, no? I know I'm slightly too paranoid about resource management but I get nervous with that lack of control.
In fact, this implementation divergence is most of the reason why `with` got added to the language, the reliance on reference counting for deterministic release of resource was ultimately seen as an issue for alternative implementations as they'd face non-memory resource exhaustion issues when trying to run existing software.
For example, the Rust docs list some platform specific examples of when the 'drop trait' (aka destructor) may not run for thread local values[0]. There are also circular references or simply another object holding a reference (directly or indirectly) that isn't clear from the context.
The `with` statement at least seems to clear up a lot of potential ambiguity if nothing else.
[0] https://doc.rust-lang.org/std/thread/struct.LocalKey.html#pl...
> forget is not marked as unsafe, because Rust's safety guarantees do not include a guarantee that destructors will always run. For example, a program can create a reference cycle using Rc, or call process::exit to exit without running destructors. Thus, allowing mem::forget from safe code does not fundamentally change Rust's safety guarantees.
They are also a source of latent bugs, which can eventually bite you as your code ages. For example, did you know it is unsafe to reference module level attributes from a destructor? Lets say your destructor does some simple cleanup using shutil.rmtree. This only works if the object's destructor is invoked before the shutil module is garbage collected at program termination. It might work now, but change the imports around or delay when the object is cleaned up (say, sticking it in a cache), and your program now spits out NameError tracebacks on termination. Which thankfully are just noise, since exceptions in destructors are ignored. Which itself is another gotcha, since you might actually want your program to exit with a failure code if it is failing.
So yes, avoid temptation and never use Python destructors. Even if you use them for the trivial things they allow or the baroque constructs required to do more complex things safely, the next person working on your code won't.
See e.g. also this bug: https://github.com/tensorflow/tensorflow/issues/22770
Which is still not really resolved. Maybe this is actually some of the problems with circular refs, and then Swig is also involved, and also multi-threading.
This isn’t exactly true. One way to still use context managers is to mix them with coroutines, even simple generator functions.
For example, for something repeatedly interacting with s database, you may have a generator function with a non-terminating loop, that relies on generator `send` commands to resume the coroutine with a newly injected value. This can be used to repeatedly insert new data or repeatedly query, and then when a “close” sentinel is sent, it breaks the loop and the context manager automatically cleans up the connection. I’ve used this idea with manipulation of TensorFlow graphs too.
This only works well if it’s hidden behind a library function or other type of user friendly calling interface though. If you’re making the user manually deal with the generator function, they are as liable to make mistakes as of they are tasked with manually closing resources or designing a destructor.
I agree this isn’t easy all the time, but it is a good trick for library writers, since you control del in that case and can design it this way so that del has no complex logic and the “real” mechanism is still just a behind the scenes context manager.
In such a case, there’s a big win to avoiding writing custom destructor logic that adds complexity and requires more testing and so on. If you can use something super simple, like a context manager decorator or a simple generator function with a context manager inside, and the destructor logic is basically just “call close()” to trigger the implicit cleanup of the context manager, it’s often worth it.
You aren’t writing much logic to specially process a “close” operation.. you’re just electing to let the thing containing the context manager go out of scope.
But I do agree in a case where, for example, leaving many open connections to a resource would create a resource limit or bottleneck, then ensuring it happens deterministically is probably important.
In nearly all cases, when we're "done" with a GUI object, we just let go of it, and let the framework or language runtime handle it. If the framework is good, it'll give you some sort of "on_gui_element_undraw" event or something you can handle to trigger your "destructor".
This results in some weirdness. For example, if you have a long living socket that is only suppose to be active for say... 3 of 5 screens of an application, now you're counting "on_gui_element_undraw" events to track GUI state to control backend state.
I've sometimes played around with the idea of wrapping these frameworks with -something- (I don't know what...) that somehow gives you like... the "history vector" of the GUI...
I don't really have fully developed thoughts on this (notably, I really only write native GUIs, and only for smallish internal tooling), but has anyone played with frameworks with more powerful state management?
You can make a rule that code should only ever invoke widget code if it “owns” that widget. Then, have the code “owning” your widget be responsible for both showing and hiding the widget. That code then always knows when to invoke cleanup for related resources.
This quickly becomes second nature and is a very intuitive way to handle, for example, database transactions.
Also, I wish Python would make exceptions and "with" easier to combine. It feels like there is always this unwelcome extra level of indentation when code may raise exceptions, and I would rather have a "try with" or something to remove that. This keeps me from using context managers for relatively simple steps that might otherwise be convenient, since it looks better to just do the simple steps as part of a try/except/finally that has to be there anyway.
Why not?
__del__ is simply an unsound part of Python.
In many cases it is perfectly reasonable to only allow context manager use. And if it isn't possible, offer explicit close() or dispose() interfaces, don't rely on __del__.
This API cleanly separates the "context manager" part of your code from the "context" part. The result of calling your function is only a context manager, with no side effects. Using `with` on that context manager yields your actual object :-)
Very simple usage:
from contextlib import contextmanager
@contextmanager
def my_context(...):
print("Started")
try:
yield 5
finally:
print("Finished")
# Calling the function alone does nothing
>>> my_context()
<contextlib._GeneratorContextManager object at 0x10de07588>
# Using `with` runs our side effects
>>> with my_context() as context:
... print(context)
...
Started
5
Finished
[1]: https://docs.python.org/3/library/contextlib.html#contextlib...