How do we handle circular dependency between Python classes?


In this article we are going to discuss how to handle the circular dependency between Python classes. First of all, let us understand what is circular dependency.

When two or more modules depend on one another, this is known as a circular dependency. This is because each module is defined in terms of the other module.

Following is an example of circular dependency

functionE():
   functionF()

And

functionF():
   functionE()

The code shown above clearly shows a circular dependency. FunctionA() calls functionB(), which depends on it, and functionB() calls functionA().There are some apparent issues with this kind of circular dependency, which we'll go over in more detail in the following section.


Problem with Circular Dependency

Circular dependencies might result in a variety of issues with your code. For instance, it can lead to a tight coupling between modules, which would restrict code reuse. This aspect also makes long-term code maintenance more challenging.

Circular dependencies can also be the cause of possible problems like memory leaks, infinite recursions, and cascading effects. It can be very challenging to troubleshoot the numerous possible issues it produces when your code contains a circular dependency, which can happen if you're not careful.

Example

When the classes of any objects involved in the circular reference have a unique __del__ function, a problem arises.Following is an example showing problem occurring with circular dependency −

class Python: def __init__(self): print("Object Python is Created") def __del__(self): print("Object Python is Destroyed") class Program: def __init__(self): print("Object Program is Created") def __del__(self): print("Object Program is Destroyed") #create the two objects Py = Python() Pr = Program() #set up the circular reference Py.Pr = Pr Pr.Py = Py #delete the objects del Py del Pr

Output

Here, both objects Py and Pr have a custom __del__ function and are holding references to one another. In the end, the __del__ methods were not called when we attempted to manually delete the object, indicating that the objects had not been destroyed and had instead caused a memory leak.

In this situation, Python's garbage collector is unable to collect objects for memory cleanup because it is unsure of what order to call the __del__ functions in.

Object Python is Created
Object Program is Created
Object Python is Destroyed
Object Program is Destroyed

Fixing Memory Leak in Circular Dependencies in Python

Circular references can lead to memory leaks, which can be avoided in two ways i.e. manually erasing each reference and utilizing the weakref() function in Python.

Manually removing each reference is not a desirable choice since weakref() eliminates the need for programmers to consider the point at which to delete the references.

In Python, the weakref function provides a weak reference that is insufficient to maintain the object's life. The item is free to be destroyed by the garbage collector so that its memory can be used by another object when the only remaining references to an object are weak references.

Example

Following example shows how to fix memory leak in circular dependency −

import weakref class Python: def __init__(self): print("Object Python is Created") def __del__(self): print("Object Python is Destroyed") class Program: def __init__(self): print("Object Program is Created") def __del__(self): print("Object Program is Destroyed") #create the two objects Py = Python() Pr = Program() #set up the weak circular reference Py.Pr = weakref.ref(Pr) Pr.Py = weakref.ref(Py) #delete the objects del Py del Pr

Output

As you can see, both of the __del__ methods were used this time, proving that the objects were successfully removed from memory.

Object Python is Created
Object Program is Created
Object Python is Destroyed
Object Program is Destroyed

Circular Dependency through circular import

The import statement in Python creates circular importing, a type of circular dependency.

Example

The following example explains this. Assume we have created 3 python files as shown below −

Example1.py

# module3 import module4 def func8(): module4.func4() def func9(): print('Welcome to TutorialsPoint')

Example2.py

# module4 import module4 def func4(): print('Thank You!') module3.func9()

Example3.py

# __init__.py import module3 module3.func8()

Python examines the module registry when importing a module to see if it has already been imported. Python utilizes the previously-existing object from cache if the module was already registered. The module registry is a table of initialized modules that is indexed using the module name. sys.modules provides access to this table.

If the module wasn't registered, Python locates it, initializes it if necessary, then executes it in the namespace of the new module.

In the above example, Python loads and runs when it reaches import module4. However, module4 is also required by module3, because defines func8().

Output

The issue arises when func4() attempts to call func9 in module3(). func9() isn't yet defined and returns an error since module3 was loaded first, which loaded module4 before it could access it −

$ python __init__.py
Thank You!
Traceback (most recent call last):
   File "__init__.py", line 3, in 
   Module3.func8()
File "C:\Users\Lenovo\Desktop\module3\__init__.py", line 5, in func8
   Module4.func4()
File "C:\Users\Lenovo\Desktop\module4\__init__.py", line 6, in func4
   module4.func9()
AttributeError: 'module' object has no attribute 'func9

Fixing the above circular dependency

Circular imports are typically the result of poor designs. A more detailed analysis of the program might have shown that the dependency isn't actually necessary or that the dependent functionality can be transferred to other modules without the circular reference.

Sometimes combining both modules into a single, larger module is a simple option.

Example

The final code from the above example would resemble the explanation given above −

# module 3 & 4 def func8(): func4() def func4(): print('Welcome to TutorialsPoint') func9() def func9(): print('Thank You!') func8()

Output

Following is an output of the above code −

Welcome to TutorialsPoint
Thank You!

Note − However, If the two modules already include a lot of code, the merged module may have some unrelated functions (tight coupling) and could grow significantly.

In the event that it doesn't work, another option would have been to delay the import of module4 and only import it when necessary. To accomplish this, include the import of module4 into the definition of func8() as follows −

# module 3 def func8(): import module4 module4.func4() def func9(): print('Thank You!')

Python will be able to load all of the functions in module3 in the above scenario and only load module4 when necessary.

It is customary but not necessary to insert all import statements at the beginning of a module (or script, for that matter)," this method does not violate Python syntax.

Updated on: 23-Nov-2022

3K+ Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements