Welcome, guest | Sign In | My Account | Store | Cart

The well-known way to avoid __del__ (and its problems when the object to be deleted is involved in a reference cycle) is to use a finalization callback on a weakref.

This recipe simply makes it easier to write such "finalizable" objects by allowing to write the finalizer as a __finalize__ method (a bit similar to a __del__ method, but not exactly as we'll see).

The first step is to call the bind_finalizer(*attrs) method on your instance; you must give bind_finalizer a list of attribute names necessary for the finalizer to operate, and it will construct a "ghost object" holding those attributes (for example, if you have a "socket" attribute you want to close, you will include "socket" in the arguments to bind_finalizer, which in turn will bind a "socket" attribute on the ghost object). The ghost object has the same class as the original self, so you will be able to call instance methods and use class variables in the finalizer as well.

Please note: the ghost object is created and its attributes are bound as soon as bind_finalizer() is called, so their values must have been properly initialized. A practical idiom is probably to call bind_finalizer() at the end of your constructor. Creating the ghost ignores any __new__ or __init__ method you have defined, so you don't have to worry about constructor signature or unwanted side effects.

Your __finalize__ method must be written as a normal method, except that it can only access those instance attributes whose names you gave to bind_finalizer. This is because __finalize__ gets a "ghost instance" instead of the original instance.

Finally, there is a remove_finalizer() which does the obvious. Call it when the resources have been somehow freed manually and automatic finalization is not useful anymore.

With this recipe, you have an object-oriented finalization scheme which still works as required when your instance is part of a reference cycle to be broken by the Garbage Collector. Also, it doesn't use metaclasses which makes it easier to re-use if you have your own metaclasses.

You can find hereafter the code for the Finalizeable class, as well as an example TransactionBase class which helps writing objects with transaction-like behaviour and optional automatic rollback on object destruction. (but the interesting stuff is really in Finalizable)

Python, 103 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
import weakref

class Finalizable(object):
    """
    Base class enabling the use a __finalize__ method without all the problems
    associated with __del__ and reference cycles.

    If you call enable_finalizer(), it will call __finalize__ with a single
    "ghost instance" argument after the object has been deleted. Creation
    of this "ghost instance" does not involve calling the __init__ method,
    but merely copying the attributes whose names were given as arguments
    to enable_finalizer().

    A Finalizable can be part of any reference cycle, but you must be careful
    that the attributes given to enable_finalizer() don't reference back to
    self, otherwise self will be immortal.
    """

    __wr_map = {}
    __wr_id = None

    def bind_finalizer(self, *attrs):
        """
        Enable __finalize__ on the current instance.
        The __finalize__ method will be called with a "ghost instance" as
        single argument.
        This ghost instance is constructed from the attributes whose names
        are given to bind_finalizer(), *at the time bind_finalizer() is called*.
        """
        cls = type(self)
        ghost = object.__new__(cls)
        ghost.__dict__.update((k, getattr(self, k)) for k in attrs)
        cls_wr_map = cls.__wr_map
        def _finalize(ref):
            try:
                ghost.__finalize__()
            finally:
                del cls_wr_map[id_ref]
        ref = weakref.ref(self, _finalize)
        id_ref = id(ref)
        cls_wr_map[id_ref] = ref
        self.__wr_id = id_ref

    def remove_finalizer(self):
        """
        Disable __finalize__, provided it has been enabled.
        """
        if self.__wr_id:
            cls = type(self)
            cls_wr_map = cls.__wr_map
            del cls_wr_map[self.__wr_id]
            del self.__wr_id


class TransactionBase(Finalizable):
    """
    A convenience base class to write transaction-like objects,
    with automatic rollback() on object destruction if required.
    """

    finished = False

    def enable_auto_rollback(self):
        self.bind_finalizer(*self.ghost_attributes)

    def commit(self):
        assert not self.finished
        self.remove_finalizer()
        self.do_commit()
        self.finished = True

    def rollback(self):
        assert not self.finished
        self.remove_finalizer()
        self.do_rollback(auto=False)
        self.finished = True

    def __finalize__(ghost):
        ghost.do_rollback(auto=True)


class TransactionExample(TransactionBase):
    """
    A transaction example which close()s a resource on rollback
    """
    ghost_attributes = ('resource', )

    def __init__(self, resource):
        self.resource = resource
        self.enable_auto_rollback()

    def __str__(self):
        return "ghost-or-object %s" % object.__str__(self)

    def do_commit(self):
        pass

    def do_rollback(self, auto):
        if auto:
            print "auto rollback", self
        else:
            print "manual rollback", self
        self.resource.close()

2 comments

Steven Bethard 14 years, 6 months ago  # | flag

Generally, this seems like a nice approach, but it should be made a little more obvious that attributes are collected at the time of the enable_finalizer() call. A naive subclass might do something like:

>>> class C(Finalizable):
...     def __init__(self):
...         self.items = [] # some resource
...         self.count = 0
...         self.enable_finalizer('items', 'count')
...     def add(self, item):
...         self.items.append(item)
...         self.count += 1
...     def __finalize__(self):
...         for i in range(self.count):
...             print 'releasing', self.items.pop()
...         print 'finalized'
...
>>> c = C()
>>> c.add('foo')
>>> c.add('bar')
>>> del c
finalized

And wonder why 'foo' and 'bar' weren't released. Perhaps the name should be changed from enable_finalizer() to store_finalizer_attributes() or something like that.

Antoine Pitrou (author) 14 years, 6 months ago  # | flag

corrected. Thanks for the comment! I've changed the API from enable_finalizer/disable_finalizer to bind_finalizer/remove_finalizer. Also, I've made clear in the doc that the attributes are bound when bind_finalizer() is called.

Antoine.

Created by Antoine Pitrou on Mon, 7 May 2007 (PSF)
Python recipes (4591)
Antoine Pitrou's recipes (1)

Required Modules

Other Information and Tasks