Summary: In this tutorial, we will learn what circular reference is, what problem does it cause and how can we handle it in Python.

What is Circular Reference in Python?

When two objects in Python holds reference of each other, it is termed as cyclic or circular reference.

This occurs when object A’s property refers to object B, and object B’s property refers back to object A.

Circular Reference UML Diagram

Note: The objects can be of the same or of different classes.

Example 1: Circular Reference between Objects of the different class

class A:
    def display(self):
        print('A: self: {0}, b:{1}'.format(hex(id(self)), hex(id(self.b))))
        
class B:
    def display(self):
        print('B: self: {0}, a:{1}'.format(hex(id(self)), hex(id(self.a))))

#two objects of different classes
a = A()
b = B()

#referring to each other
a.b = b
b.a = a

#display the circular reference
a.display()
b.display()

Output:

A: self: 0x7fd69c553d00, b:0x7fd69c4c4ca0
B: self: 0x7fd69c4c4ca0, a:0x7fd69c553d00

Example 2: Circular Reference between Objects of the same class

class X(str):
    def display(self, second):
        print(f"{self}: self: {hex(id(self))}, {second}: {hex(id(second))}")

#two objects of the same class
a = X('A')
b = X('B')

#referring each other
b.a = a               
a.b = b

#display the circular reference
a.display(a.b)
b.display(b.a)

Output:

A: self: 0x7f26fcd69580, B:0x7f26fcd69510
B: self: 0x7f26fcd69510, A:0x7f26fcd69580

Notice, each of the two objects is storing the reference of the other as a property, thus forming a circular reference.

What Problem arises due to Circular Reference?

The problem occurs when the classes of any objects involved in the circular reference have a custom __del__ function.

Consider the following Python code for instance:

class A:
    def __init__(self):
        print("Object A Created")
        
    def __del__(self):
        print("Object A Destroyed")
        
class B:
    def __init__(self):
        print("Object B Created")
        
    def __del__(self):
        print("Object B Destroyed")

#creating two objects
a = A()
b = B()

#setting up circular reference
a.b = b
b.a = a

#deleting objects
del a
del b

Output:

Object A Created
Object B Created

Here, both objects a and b are holding reference of the other and has a custom __del__ function.

In the end, when we try to delete the object manually, the __del__ methods were not called, indicating that the objects were not destroyed, thus causing a memory leak.

In such a case, the garbage collector of Python gets confused as to what order it should call the __del__ methods and therefore it is not able to collect the objects for the clearance of the memory,

Although this problem is resolved as PEP 442 in Python 3.4, it still exists in Python versions < 3.4.

How to Handle Memory Leak in Circular Reference?

We can prevent memory leaks caused by circular references in two ways::

  1. Manually deleting each reference.
  2. Using weakref.

Manually deleting each reference is not a good option because we as programmers would have to think of the point where we should delete the references, whereas in the case of weakref we don’t have to.

weakref in Python creates a weak reference that is not enough to keep the object alive. The garbage collector is free to destroy the object and reuse its memory for something else (when the only remaining references to an object are weak references).

https://docs.python.org/

Let’s rewrite the code using weak reference and observe the output:

import weakref

class A:
    def __init__(self):
        print("Object A Created")
        
    def __del__(self):
        print("Object A Destroyed")
        
class B:
    def __init__(self):
        print("Object B Created")
        
    def __del__(self):
        print("Object B Destroyed")

#creating two objects
a = A()
b = B()

#setting up weak circular reference
a.b = weakref.ref(b)
b.a = weakref.ref(a)

#deleting objects
del a
del b

Output:

Object A Created
Object B Created
Object A Destroyed
Object B Destroyed

As you can see, this time both the __del__ methods were called, indicating that the objects were successfully deleted from the memory.

Conclusion

When two objects in Python holds reference of each other, it is known as circular reference.

Circular reference could cause memory leak in Python versions below 3.4.

We as good programmers, should use weak references to create circular reference to prevent potential leak of memory.

This Post Has One Comment

Leave a Reply