5

I understand that the __del__ function of a Python class is not treated in the way that many people might expect: as a destructor.

I also understand that there are more 'pythonic' and arguably more elegant ways to tidy up, particularly with use of the with construct.

However, when writing code that may be used by an audience less versed in pythonic ways, when cleanup is important, is there an elegant way I can simply get __del__ to work as a destructor reliably, without interfering with python's natural use of __del__?

The expectation that __del__ behave as a destructor doesn't seem unreasonable and at the same time is quite common. So I'm simply wondering if there is an elegant way to make it work as per that expectation - disregarding the many debates that can be had over the merits of how pythonic it is.

QA Collective
  • 2,222
  • 21
  • 34

2 Answers2

4

If you understand all that, why not do it in the Pythonic way? Compare another class where cleanup is important: tempfile.TemporaryDirectory.

with TemporaryDirectory() as tmp:
    # ...
# tmp is deleted

def foo():
    tmp = TemporaryDirectory()
foo()
# tmp is deleted

How do they do this? Here's the relevant bit:

import weakref
class Foo():
    def __init__(self, name):
        self.name = name
        self._finalizer = weakref.finalize(self, self._cleanup, self.name)
        print("%s reporting for duty!" % name)

    @classmethod
    def _cleanup(cls, name):
        print("%s feels forgotten! Bye!" % name)

    def cleanup(self):
        if self._finalizer.detach():
            print("%s told to go away! Bye!" % self.name)

def foo():
    print("Calling Arnold")
    tmpfoo = Foo("Arnold")
    print("Finishing with Arnold")

foo()
# => Calling Arnold
# => Arnold reporting for duty
# => Finishing with Arnold
# => Arnold feels forgotten. Bye!

def bar():
    print("Calling Rocky")
    tmpbar = Foo("Rocky")
    tmpbar.cleanup()
    print("Finishing with Rocky")

bar()
# => Calling Rocky
# => Rocky reporting for duty!
# => Rocky told to go away! Bye!
# => Finishing with Rocky

weakref.finalize will trigger _cleanup when the object is garbage-collected, or at the end of the program if it's still around. We can keep the finaliser around so that we can explicitly kill the object (using detach) and mark it as dead so the finaliser is not called (when we want to manually handle the cleanup).

If you want to support the context usage with with, it is trivial to add __enter__ and __exit__ methods, just invoke cleanup in __exit__ ("manual cleanup" as discussed above).

Amadan
  • 191,408
  • 23
  • 240
  • 301
  • 1
    Any particular reason why you made `_cleanup` a classmethod? – Aran-Fey Jul 25 '18 at 08:09
  • 3
    @Aran-Fey: Indeed: [Docs](https://docs.python.org/3/library/weakref.html#weakref.finalize): "**Note:** It is important to ensure that `func`, `args` and `kwargs` do not own any references to `obj`, either directly or indirectly, since otherwise `obj` will never be garbage collected. In particular, `func` should not be a bound method of `obj`." – Amadan Jul 25 '18 at 08:12
  • So that means you don't have access to the instance in the finalizer? That's pretty useless then, isn't it? Is there a way to pass the instance to the finalizer? – Aran-Fey Jul 25 '18 at 08:17
  • 1
    @Aran-Fey: That defeats the point. Pass any information relevant to the finalizer through args/kwargs, like `name` here. I'd argue that if it were useless it would not be used (and it obviously is, for example in the cited `TemporaryDirectory` class). – Amadan Jul 25 '18 at 08:19
  • How does that defeat the point? If I need information about the instance that's being deleted, I don't want to have to pass all the required values as arguments. There's no reason why that should be necessary. Having access to the instance would be the easiest, cleanest, and least error-prone solution. – Aran-Fey Jul 25 '18 at 08:26
  • 2
    @Aran-Fey: "There's no reason why that should be necessary." Whoever wrote Python documentation surely thought it was critical. For one thing, if you have a live reference to the object, it cannot get garbage collected, so the weakref finaliser never gets to trigger; that's a pretty good reason why it's necessary. Still, there's quite a big discussion on `finalize` vs `__del__` on the bottom of the linked page, you might find that if you really need something from the object that you won't know about at the time you register the finaliser, you might risk going the `__del__` and `atexit` route. – Amadan Jul 25 '18 at 08:34
  • Just affirming the interesting "Comparing finalizers with __del__() methods" section in the [`weakref` docs](https://docs.python.org/3/library/weakref.html#comparing-finalizers-with-del-methods), with updates for Python >= 3.4. (`__del__()` is safe to use again but `weakref.finalize` has other advantages.) – David Dec 07 '20 at 05:01
-1

This is a pattern I have been employing that achieves this using the atexit python module.

class Demo(object):
    def __init__(self, *args, **kwargs):
        import atexit
        atexit.register(self.__del__)

    def __del__(self):
        print("__del__ being called!")

t1 = Demo()
t2 = Demo()

quit()

When pasted into a python command prompt, this is the total output:

Python 3.6.0 (v3.6.0:41df79263a11, Dec 23 2016, 08:06:12) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> class Demo(object):
...     def __init__(self, *args, **kwargs):
...         import atexit
...         atexit.register(self.__del__)
...
...     def __del__(self):
...         print("__del__ being called!")
...
>>> t1 = Demo()
>>> t2 = Demo()
>>>
>>> quit()
__del__ being called!
__del__ being called!
QA Collective
  • 2,222
  • 21
  • 34