Circular references and the weakref module

In cases where we need circular references but also want __del__() to work nicely, we can use weak references. One common use case for circular references is mutual references: a parent with a collection of children where each child has a reference back to the parent. If a Player class has multiple hands, it might be helpful for a Hand object to contain a weak reference to the owning Player class.

The default object references could be called strong references; however, direct references is a better term. They're used by the reference-counting mechanism in Python; they cannot be ignored.

Consider the following statement:

a = B() 

The a variable has a direct reference to the object of the B class that was created. The reference count to the instance of B is at least one, because the a variable has a reference.

A weak reference involves a two-step process to find the associated object. A weak reference will use x.parent(), invoking the weak reference as a callable object to track down the actual parent object. This two-step process allows the reference counting or garbage collection to remove the referenced object, leaving the weak reference dangling.

The weakref module defines a number of collections that use weak references instead of strong references. This allows us to create dictionaries that, for example, permit the garbage collection of otherwise unused objects.

We can modify our Parent and Child classes to use weak references from Child to Parent, permitting simpler destruction of unused objects.

The following is a modified class that uses weak references from Child to Parent:

from weakref import ref


class Parent2:

def __init__(self, *children: 'Child2') -> None:
for child in children:
child.parent = ref(self)
self.children = {c.id: c for c in children}

def __del__(self) -> None:
print(
f"Removing {self.__class__.__name__} {id(self):d}"
)

class Child2:

def __init__(self, id: str) -> None:
self.id = id
self.parent: ref[Parent2] = cast(ref[Parent2], None)

def __del__(self) -> None:
print(
f"Removing {self.__class__.__name__} {id(self):d}"
)

We've changed the child to parent reference to be a weakref object reference instead of a simple, direct reference.

From within a Child class, we must locate the parent object via a two-step operation:

p = self.parent() 
if p is not None: 
    # Process p, the Parent instance.
else: 
    # The Parent instance was garbage collected. 

We should explicitly check to be sure the referenced object was found. Objects with weak references can be removed, leaving the weak reference dangling – it not longer refers to an object in memory. There are several responses, which we'll look at below.

When we use this new Parent2 class, we see that del makes the reference counts go to zero, and the object is immediately removed:

>>> p = Parent2(Child(), Child()) 
>>> del p 
Removing Parent2 4303253584 
Removing Child 4303256464 
Removing Child 4303043344 

When a weakref reference is dangling (because the referent was destroyed), we have several potential responses:

  • Recreate the referent. You could, perhaps, reload it from a database.
  • Use the warnings module to write the debugging information in low-memory situations where the garbage collector removed objects unexpectedly and try to continue in degraded mode.
  • Ignore it.

Generally, weakref references are left dangling after objects have been removed for very good reasons: variables have gone out of scope, a namespace is no longer in use, or the application is shutting down. For these kinds of reasons, the third response is quite common. The object trying to create the reference is probably about to be removed as well.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset