Article Categories
- All Categories
-
Data Structure
-
Networking
-
RDBMS
-
Operating System
-
Java
-
MS Excel
-
iOS
-
HTML
-
CSS
-
Android
-
Python
-
C Programming
-
C++
-
C#
-
MongoDB
-
MySQL
-
Javascript
-
PHP
-
Economics & Finance
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.
What is Circular Dependency?
Following is an example of circular dependency ?
functionE(): functionF()
And
functionF(): functionE()
The code shown above clearly shows a circular dependency. FunctionE() calls functionF(), which depends on it, and functionF() calls functionE(). There are some apparent issues with this kind of circular dependency, which we'll go over in more detail in the following section.
Problems 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.
Memory Leak Example
When the classes of any objects involved in the circular reference have a custom __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_obj = Python()
pr_obj = Program()
# Set up the circular reference
py_obj.pr_ref = pr_obj
pr_obj.py_ref = py_obj
# Delete the objects
del py_obj
del pr_obj
The output of the above code is ?
Object Python is Created Object Program is Created
Here, both objects 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 objects, 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.
Fixing Memory Leak with weakref
Circular references can lead to memory leaks, which can be avoided using the weakref() function in Python. In Python, the weakref function provides a weak reference that is insufficient to maintain the object's life. The object is free to be destroyed by the garbage collector 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_obj = Python()
pr_obj = Program()
# Set up the weak circular reference
py_obj.pr_ref = weakref.ref(pr_obj)
pr_obj.py_ref = weakref.ref(py_obj)
# Delete the objects
del py_obj
del pr_obj
The output of the above code is ?
Object Python is Created Object Program is Created Object Python is Destroyed Object Program is Destroyed
As you can see, both of the __del__ methods were called this time, proving that the objects were successfully removed from memory.
Circular Import Dependencies
The import statement in Python creates circular importing, a type of circular dependency. This happens when two modules try to import each other.
Problematic Example
The following example explains this. Assume we have created 3 python files as shown below ?
module3.py
# module3.py
import module4
def func8():
module4.func4()
def func9():
print('Welcome to TutorialsPoint')
module4.py
# module4.py
import module3
def func4():
print('Thank You!')
module3.func9()
main.py
# main.py import module3 module3.func8()
The issue arises when func4() attempts to call func9() in module3. Since module3 was loaded first and loaded module4 before it could complete initialization, func9() may not be accessible yet.
Fixing Circular Import Dependencies
Circular imports are typically the result of poor designs. Here are two common solutions:
Method 1: Combine Modules
Sometimes combining both modules into a single, larger module is a simple option ?
# combined_module.py
def func8():
func4()
def func4():
print('Thank You!')
func9()
def func9():
print('Welcome to TutorialsPoint')
# Execute the functions
func8()
The output of the above code is ?
Thank You! Welcome to TutorialsPoint
Method 2: Local Import
Another option is to delay the import and only import it when necessary. Move the import statement inside the function ?
# module3.py
def func8():
import module4 # Import only when needed
module4.func4()
def func9():
print('Welcome to TutorialsPoint')
Python will be able to load all of the functions in module3 and only load module4 when necessary. This method does not violate Python syntax, though it is customary to place import statements at the beginning of a module.
Conclusion
Circular dependencies can cause memory leaks and import errors. Use weakref to break reference cycles between objects, and restructure code to avoid circular imports through better design or local imports.
