Search code examples
pythonpython-3.xweak-references

Does circular reference between two objects requires the use of weakref?


I am trying to implement something that works on the principle below:

from weakref import WeakValueDictionary

class Container(object):
    def __init__(self):
        self.dic = WeakValueDictionary({})
    def put_in(self, something):
        self.dic[something] = Thing(self, something)

class Thing(object):
    def __init__(self, container, name):
        self.container = container
        self.name = name

    def what_I_am(self):
        print("I am a thing called {}".format(self.name))

pot = Container()
pot.put_in('foo')
pot.dic['foo'].what_I_am()

But I get :

  File "C:/Users/jacques/ownCloud/dev/weakref.py", line 26, in <module>
    pot.dic['foo'].what_I_am()
  File "C:\Program Files\Anaconda3\lib\weakref.py", line 131, in __getitem__
    o = self.data[key]()
KeyError: 'foo'

I understand that my implementation is not correct because Thing instance gets GCed and deleted from the WeakValueDictionary.

Is there any way I could achieve something like this to prevent the circular reference between Container and Thing ?

Edit : If I change the code above for the one below, would it solve the circular reference issue ?

from weakref import proxy

class Container(dict):
    def put_in(self, something):
        self[something] = Thing(self)

class Thing(object):
    def __init__(self, container):
        self.container = proxy(container)

    def what_is_it(self):
        print("I am a thing called {}".format(self))

    def __getattr__(self, name):
        try: #Look up the Thing instance first
            return object.__getattribute__(self, name)
        except AttributeError: #Try to find the attribute in container
            return self.container.__getattribute__(name)

    def __format__(self, spec):
        (name,) = (key for key, val in self.container.items() if self == val)
        return name

pot = Container()
pot.location = 'Living room'
pot.put_in('foo')
pot['foo'].what_is_it()
print(pot['foo'].location)

Solution

  • You do not need to worry about circular references. Python is fully capable of managing its own memory in this case. And will delete objects with circular references as and when necessary.

    Your implemenation need only look like this:

    class Container(dict):
        def put_in(self, something):
            self[something] = Thing(self, something)
    
    class Thing:
        def __init__(self, container, name):
            self.container = container
            self.name = name
    
        def what_is_it(self):
            assert self.container[self.name] is self, "Thing stored under wrong name"
            print("I am a thing called {}".format(self.name))
    
        def __getattr__(self, name):
            # By the time __getattr__ is called, normal attribute access on Thing has
            # already failed. So, no need to check again. Go straight to checking the 
            # container
            try:
                return getattr(self.container, name)
            except AttributeError:
                # raise a fresh attribute error to make it clearer that the 
                # attribute was initially accessed on a Thing object
                raise AttributeError("'Thing' object has no attribute {!r}".format(name)) from e
    

    A quick test to show you how things work:

    c = Container()
    c.put_in("test")
    c.value = 0
    
    # Attribute demonstration
    c["test"].what_is_it()
    t = c["test"]
    print("name:", t.name) # get a Thing attribute
    print("value:", t.value) # get a Container Attribute
    c.name = "another name"
    print("Thing name:" t.name) # gets Thing attrs in preference to Container attrs
    
    # Garbage collection demonstration
    import weakref
    import gc
    
    r = weakref.ref(c["test"])
    del c, t
    # no non-weak references to t exist anymore
    print(r()) # but Thing object not deleted yet
    # collecting circular references is non-trivial so Python does this infrequently
    
    gc.collect() # force a collection
    print(r()) # Thing object has now been deleted