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

This is an ever-so-slightly over engineered solution to using weakrefs instead of __del__. It provides a "core" object for all the attributes your cleanup code needs, then allows your main object to continue as normal by using descriptors.

Python, 163 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python
"""
>>> class Demo(safedel):
...     __coreattrs__ = ['count']
...     bird = 'European'
...     def __init__(self):
...         super(Demo, self).__init__()
...         self.count = 5
...     @coremethod
...     def __safedel__(core):
...         super(Demo.__coreclass__, core).__safedel__()
...         print "Count:", core.count
...     @coremethod
...     def bridgekeeper(core):
...         # core instances can also access class attributes of the
...         # original class
...         return "%s swallow" % core.bird

>>> d = Demo()

# The core can still access attributes of the main class
>>> print d.bridgekeeper()
European swallow
>>> Demo.bird = 'African'
>>> print d.bridgekeeper()
African swallow

# Any attribute listed in __coreattr__ goes to the core object instead
>>> print d.count
5
>>> d.count = 3
>>> del d
Count: 3
"""

import weakref

__all__ = ['safedel', 'coremethod']


try:
    reflist = set()  # python2.4.  More efficient
except:
    class FakeSet(dict):
        def add(self, item):
            self[item] = None
        def remove(self, item):
            del self[item]
    reflist = FakeSet()  # Python2.2 or Python2.3


class SafedelMetaclass(type):
    def __init__(cls, name, bases, newattrs):
        #super(SafedelMetaclass, cls).__init__(name, bases, newattrs)
        type.__init__(name, bases, newattrs)

        attrs = {}
        for base in bases:
            for attrname in getattr(base, '__coreattrs__', []):
                attrs[attrname] = None
        for attrname in newattrs.get('__coreattrs__', []):
            attrs[attrname] = None

        def coregetattr(core, name):
            return getattr(core.__surfaceclass__, name)

        corebases = []
        for base in bases:
            x = getattr(base, '__coreclass__', None)
            if x is not None:
                corebases.append(x)
        corebases = tuple(corebases)

        newcoreattrs = {}
        newcoreattrs['__surfaceclass__'] = cls
        newcoreattrs['__getattr__'] = coregetattr

        for key, value in newattrs.items():
            if isinstance(value, coremethod):
                # coremethods need to know what name they have, but they
                # don't trust the info available when created, so we
                # supply it here instead.
                value.name = key
                newcoreattrs[key] = value.func

        coreclass = type(name + '__Core', corebases, newcoreattrs)

        cls.__coreclass__ = coreclass
        for attrname in attrs:
            setattr(cls, attrname, CoreAttrProxy(attrname))


class CoreAttrProxy(object):
    __slots__ = ['name']
    def __init__(self, name):
        self.name = name

    def __get__(self, obj, objtype):
        if obj is None:  # Called from the class, somehow...
            raise AttributeError, 'Attribute %s not found' % self.name
        return object.__getattribute__(obj.__core__, self.name)

    def __set__(self, obj, value):
        object.__setattr__(obj.__core__, self.name, value)

    def __delete__(self, obj):
        # Slots erroneously don't raise AttributeError if the attribute
        # doesn't exist, so we use getattr to raise AttributeError anyway.
        getattr(obj.__core__, self.name)
        object.__delattr__(obj.__core__, self.name)


class coremethod(object):
    __slots__ = ['func', 'name']
    # name is set by SafedelMetaclass

    def __init__(self, func):
        try:
            if func.im_self is None:
                func = func.im_func  # Skip through unbound methods
        except AttributeError:
            pass  # Not a method at all
        self.func = func

    def __get__(self, obj, objtype):
        if obj is None:
            return self.func
        else:
            def boundcoremethod(*args, **kwargs):
                return getattr(obj.__core__, self.name)(*args, **kwargs)
            return boundcoremethod


class safedel(object):
    __metaclass__ = SafedelMetaclass
    def __init__(self, *args, **kwargs):
        super(safedel, self).__init__(*args, **kwargs)
        self.__core__ = self.__coreclass__()

        def outer(core, cls):
            def inner(ref):
                reflist.remove(ref)
                try:
                    cls.__safedel__(core)
                except:
                    import traceback
                    traceback.print_exc()
            return inner
        reflist.add(weakref.ref(self, outer(self.__core__, self.__class__)))

    def __safedel__(core):
        f = getattr(super(safedel.__coreclass__, core), '__safedel__', None)
        if f:
            f.__safedel__()
    __safedel__ = coremethod(__safedel__)


def _test():
    import doctest
    doctest.testmod()

if __name__ == '__main__':
    _test()

The problems with __del__ are well known: CPython won't delete any objects involved in a cycle if they contain a __del__ method. This is due to the inherent complexity in resurrecting an object so it can perform cleanup. The alternative approach is to ensure the cleanup code was never in the cycle in the first place, and that's what weakref can provide.

Here I provide an "ultimate solution". The metaclass does all the work, all you need to do is slap on __coreattrs__, define a __safedel__() method, and use @coremethod where necessary.

If defining a type that can also be explicitly cleaned up you should have an idempotent cleanup() coremethod method that is called by your __safedel__().

Warning: although safedel class should work in Python 2.2 and 2.3, the doctests included do not. Caveat emptor.

Also, I wrote this code years ago, and although I've cleaned it up before posting, I fully expect someone to point out a glaring oversight. :)

4 comments

Steven Bethard 16 years, 11 months ago  # | flag

coremethod.__get__. Seems like coremethod.__get__ could just be:

def __get__(self, obj, objtype):
    if obj is None:
        return self.func
    else:
        return self.func.__get__(obj.__core__)
Steven Bethard 16 years, 11 months ago  # | flag

CoreAttrProxy. CoreAttrProxy also seems overly complex. The following still passes your doctests:

class CoreAttrProxy(object):
    __slots__ = ['name']
    def __init__(self, name):
        self.name = name
    def __get__(self, obj, objtype):
        return getattr(obj.__core__, self.name)
    def __set__(self, obj, value):
        setattr(obj.__core__, self.name, value)
    def __delete__(self, obj):
        delattr(obj.__core__, self.name)
Steven Bethard 16 years, 11 months ago  # | flag

merging safedel.__init__ into SafedelMetaclass. I like pushing the logic out of safedel and into the metaclass. Here's one way to do that:

class SafedelMetaclass(type):
    # list of weak references; when the objects they reference
    # disappear, their callbacks will remove them from this set
    _reflist = set()

    def __init__(cls, name, bases, class_dict):
        super(SafedelMetaclass, cls).__init__(name, bases, class_dict)

        # create the core class
        core_bases = []
        for base in bases:
            x = getattr(base, '__coreclass__', None)
            if x is not None:
                core_bases.append(x)
        core_class = type(name + '__Core', tuple(core_bases), {})
        core_class.__surfaceclass__ = cls

        # add core class __getattr__
        def core_getattr(core, name):
            return getattr(core.__surfaceclass__, name)
        core_class.__getattr__ = core_getattr

        # add core class methods (unwrapping coremethod objects)
        for attr_name, attr_value in class_dict.items():
            if isinstance(attr_value, coremethod):
                setattr(core_class, attr_name, attr_value.func)

        # add core class and attribute proxies to main class
        cls.__coreclass__ = core_class
        for attr_name in class_dict.get('__coreattrs__', []):
            setattr(cls, attr_name, CoreAttrProxy(attr_name))

    def __call__(cls, *args, **kwargs):
        # create a new instance, set __core__, then initialize
        obj = cls.__new__(cls, *args, **kwargs)
        obj.__core__ = obj.__coreclass__()
        obj.__init__(*args, **kwargs)

        # a closure for calling the class's __safedel__ method,
        # passing it the instance's core object
        def get_del_caller(core, cls):
            def del_caller(ref):
                SafedelMetaclass._reflist.remove(ref)
                cls.__safedel__(core)
            return del_caller

        # register a weak reference to call the __safedel__
        del_caller = get_del_caller(obj.__core__, obj.__class__)
        SafedelMetaclass._reflist.add(weakref.ref(obj, del_caller))

        # return the newly created instance
        return obj

With this approach, you don't even really need the safedel class (though it's probably handy as a simple base class).

Benjamin Peterson 13 years, 11 months ago  # | flag

This has the unfortunate property of not working when two objects using this compare and hash equal to each other. This is because weakref.ref inherits the hash and equality from its referrent.