Circular references and garbage collection

The following is a common situation that involves circularity. One class, Parent, contains a collection of children. Each Child instance contains a reference to the Parent class. We'll use these two classes to examine circular references:

class Parent:

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

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


class Child:

def __init__(self, id: str) -> None:
self.id = id
self.parent: Parent = cast(Parent, None)

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

A Parent instance has a collection of children in a simple dict. Note that the parameter value, *children, has a type hint of 'Child'. The Child class has not been defined yet. In order to provide a type hint, mypy will resolve a string to a type that's defined elsewhere in the module. In order to have a forward reference or a circular reference, we have to use strings instead of a yet-to-be-defined type.

Each Child instance has a reference to the Parent class that contains it. The reference is created during initialization, when the children are inserted into the parent's internal collection.

We've made both classes noisy so that we can see when the objects are removed. The following is what happens:

>>> p = Parent(Child('a'), Child('b'))
>>> del p

The Parent instance and two initial Child instances cannot be removed. They both contain references to each other. Prior to the del statement, there are three references to the Parent object. The p variable has one reference. Each child object also has a reference. When the del statement removed the p variable, this decremented the reference count for the Parent instance. The count is not zero, so the object remains in memory, unusable. We call this a memory leak.

We can create a childless Parent instance, as shown in the following code snippet:

>>> p_0 = Parent() 
>>> id(p_0) 
4313921744 
>>> del p_0 
Removing Parent 4313921744 

This object is deleted immediately, as expected.

Because of the mutual or circular references, a Parent instance and its list of Child instances cannot be removed from the memory. If we import the garbage collector interface, gc, we can collect and display these nonremovable objects.

We'll use the gc.collect() method to collect all the nonremovable objects that have a __del__() method, as shown in the following code snippet:

>>> import gc 
>>> gc.collect() 
Removing Child 4536680176
Removing Child 4536680232
Removing Parent 4536679952
30

We can see that our Parent object is cleaned up by the manual use of the garbage collector. The collect() function locates objects that are inaccessible, identifies any circular references, and forces their deletion.

Note that we can't break the circularity by putting code in the __del__() method. The __del__() method is called after the circularity has been broken and the reference counts are already zero. When we have circular references, we can no longer rely on simple Python reference counting to clear out the memory of unused objects. We must either explicitly break the circularity or use a weakref reference, which permits garbage collection.

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

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