Weak Attributes
June 04, 2006

Weak attributes are attributes of an instance that hold only a weak reference to another object. They are very useful for automatic breaking of cyclic references. This weakattr class implements a data-descriptor that holds a weak reference to the attribute, so that when it's no longer strongly-referenced, it automatically "disappears" from the instance.

The motivation for this concept comes from http://mail.python.org/pipermail/python-3000/2006-June/002357.html

Code

import weakref

class weakattr(object):
    """
    weakattr - a weakly-referenced attribute. When the attribute is no longer
    referenced, it 'disappears' from the instance. Great for cyclic references.
    """
    __slots__ = ["dict", "errmsg"]
    
    def __init__(self, name = None):
        self.dict = weakref.WeakValueDictionary()
        if name:
            self.errmsg = "%%r has no attribute named %r" % (name,)
        else:
            self.errmsg = "%r has no such attribute"
    
    def __repr__(self):
        return "<weakattr at 0x%08X>" % (id(self),)
    
    def __get__(self, obj, cls):
        if obj is None:
            return self
        try:
            return self.dict[id(obj)]
        except KeyError:
            raise AttributeError(self.errmsg % (obj,))
    
    def __set__(self, obj, value):
        self.dict[id(obj)] = value
    
    def __delete__(self, obj):
        try:
            del self.dict[id(obj)]
        except KeyError:
            raise AttributeError(self.errmsg % (obj,))

Example

A simple example

>>> class x(object):
...     cyc = weakattr()
...
...     def __init__(self):
...         self.cyc = self
...     def __del__(self):
...         print "g'bye"
...         print hasattr(self, "cyc") # will print False
...
>>> y = x()
>>> y
<__main__.x object at 0x009EEAF0>
>>> y.cyc
<__main__.x object at 0x009EEAF0>
>>> y.cyc.cyc
<__main__.x object at 0x009EEAF0>
>>> del y
>>> gc.collect() # force a collection
g'bye            # printed by y.__del__
False            # printed by y.__del__
0                # return value of gc.collect
[[code]]

In case you wondered what the optional  parameter means
[[code]]
>>> class x(object):
...     attr1 = weakattr()
...     attr2 = weakattr("attr2")
...
>>>
>>> y = x()
>>> # will not show a name
...
>>> y.attr1 
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 24, in __get__
AttributeError: <__main__.x object at 0x009EEDD0> has no such attribute
>>>
>>> # will show 'attr2'
...
>>> y.attr2 
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 24, in __get__
AttributeError: <__main__.x object at 0x009EEDD0> has no attribute named 'attr2'
>>>

And a somewhat more complex one:

>>> class Node(object):
...     next = weakattr()
...
...     def __init__(self, name):
...         self.name = name
...     def __del__(self):
...         print "g'bye", self
...     def __repr__(self):
...         return "<Node %s>" % (self.name,)
...
>>>
>>> node1 = Node("node1")
>>> node2 = Node("node2")
>>> node1.next = node2
>>> node2.next = node1
>>>
>>> node1
<Node node1>
>>> node1.next
<Node node2>
>>> node1.next.next
<Node node1>
>>> node1.next.next.next
<Node node2>
>>>
>>> del node2
>>> # forcibly collect the dead objects
... # this will cause node1.next to disappear, so that 
... # node2._del__ is be called
>>> gc.collect() 
g'bye <Node node2>
0