[Python-3000] Removing __del__

Nick Coghlan ncoghlan at gmail.com
Tue Sep 26 16:12:10 CEST 2006


Giovanni Bajo wrote:
> Raymond Hettinger wrote:
> 
>> In short, __del__ should disappear not because it is useless but
>> because
>> it is hazardous.  The consenting adults philosophy means that we don't
>> put-up artificial barriers to intentional hacks, but it does not mean
>> that we bait the hook and leave error-prone traps for the unwary.  In
>> Py3k, I would like to see explicit finalization as a preferred
>> approach
>> and for weakrefs be the one-way-to-do-it for designs with implicit
>> finalization.
> 
> Raymond, there is one thing I don't understand in your line of reasoning. You
> say that you prefer explicit finalization, but that implicit finalization still
> needs to be supported. And for that, you'd rather drop __del__ and use
> weakrefs. But why? You say that __del__ is harardous, but I can't see how
> weakrefs are less hazardous.

As I see it, __del__ is more hazardous because it's an attractive nuisance - 
it *looks* like it should be easy to use, but I'm willing to bet that a lot of 
the __del__ methods implemented in the wild are either actual or potential 
bugs. For example, it would be easy for a maintenance programmer to make a 
change to include a reference in a data structure from a child node back to 
its parent node to address a problem, and suddenly the application's memory 
usage goes through the roof due to uncollectable cycles. Even the initial 
implementation of the generator __del__ slot in the *Python 2.5 core* was 
buggy, leading to such cycles - if the developers of the Python interpreter 
find it hard to get __del__ right, then there's something seriously wrong with 
it in its current form.

By explicitly stating that __del__ will go away in Py3k, with the current 
intent being to replace it with explicit finalization (via with statements) 
and the implicit finalization offered by weakref callbacks, it encourages 
people to look for ways to make the API for the latter easier to use.

For example, a "finalizer" factory function could be added to weakref:

_finalizer_refs = set()
def finalizer(*args, **kwds):
     """Create a finalizer from an object, callback and keyword dictionary"""
     # Use positional args and a closure to avoid namespace collisions
     obj, callback = args
     def _finalizer(_ref=None):
         """Callable that invokes the finalization callback"""
         # Use closure to get at weakref to allow direct invocation
         # This creates a cycle, so this approach relies on cyclic GC
         # to clean up the finalizer objects!
         try:
             _finalizer_refs.remove(ref)
         except KeyError:
             pass
         else:
             callback(_finalizer)
     # Give callback access to keyword arguments
     _finalizer.__dict__ = kwds
     ref = weakref.ref(obj, _finalizer)
     _finalizer_refs.add(ref)
     return _finalizer

Example usage:

from weakref import finalizer

class Wrapper(object):
     def __init__(self, x=1):
         self._data = finalizer(self, self.finalize, x=x)
     @staticmethod
     def finalize(data):
         print "Finalizing: value=%s!" % data.x
     def get_value(self):
         return self._data.x
     def increment(self, by=1):
         self._data.x += by
     def close(self):
         self._data()  # Explicitly invoke the finalizer
         self._data = None

 >>> test = Wrapper()
 >>> test.get_value()
1
 >>> test.increment(2)
 >>> test.get_value()
3
 >>> del test
Finalizing: value=3!
 >>> test = Wrapper()
 >>> test.get_value()
1
 >>> test.increment(2)
 >>> test.get_value()
3
 >>> test.close()
Finalizing: value=3!
 >>> del test

For comparison, here's the __del__ based version (which has the downside of 
potentially giving the cyclic GC fits if other attributes are added to the 
object):

class Wrapper(object):
     def __init__(self, x=1):
         self._x = x
     def __del__(self):
         if self._x is not None:
             print "Finalizing: value=%s!" % self._x
     def get_value(self):
         return self._x
     def increment(self, by=1):
         self._x += by
     def close(self):
         self.__del__()
         self._x = None

Not counting the import line, both versions are 13 lines long (granted, the 
weakref version would be a bit longer if the finalizer needed access to public 
attributes - in that case, the weakref version would need to use properties to 
hide the existence of the finalizer object).

Cheers,
Nick.

P.S. the central finalizers list also works a treat for debugging why objects 
aren't getting finalized as expected - a simple loop like "for wr in 
weakref.finalizers: print gc.get_referrers(wr)" after a gc.collect() call 
works pretty well.

-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://www.boredomandlaziness.org


More information about the Python-3000 mailing list