MRO Method Resolution Order in Python

Method Resolution Order (MRO) is a critical concept in object-oriented programming, particularly in Python, which determines the order in which classes are searched when looking for a method in a class hierarchy.

This order is especially important when working with multiple inheritance, where a class inherits from more than one parent class. Python uses a specific mechanism, known as C3 Linearization, to resolve method lookup conflicts and provide a consistent method resolution strategy.

In this article, we’ll explore the importance of MRO in Python, how it works, and why understanding it is crucial for writing clear and efficient object-oriented code.

What is MRO?

In Python, when a method is called on an object, the interpreter needs to determine which class contains the method definition. This becomes more complex in cases of multiple inheritance, where a class inherits from more than one base class. The Method Resolution Order (MRO) is the sequence of classes Python follows when searching for a method in such scenarios.

The MRO defines the order in which Python searches for a method starting from the current class and moving through its base classes, respecting the class hierarchy. This ensures that the method resolution is deterministic and follows a consistent rule, preventing ambiguity and errors.

python mro

The C3 Linearization Algorithm

Python uses the C3 Linearization algorithm to determine the MRO. This algorithm linearizes the inheritance graph and creates an order in which classes should be checked for method resolution. C3 Linearization ensures that the class hierarchy is respected, avoiding conflicts when multiple base classes have methods with the same name.

For example, if a class inherits from two classes that have conflicting method definitions, the C3 Linearization will define an unambiguous order in which the classes are searched. This order is crucial when using the super() function to call methods from parent classes in a multiple inheritance scenario.

How does Python determine the MRO?

The MRO is determined when a class is defined and can be accessed via the mro() method. The mro() method returns a list of classes in the order they are searched when a method is called. You can use this method to see the MRO for any given class.

class A:
    def method(self):
        print("Method in A")

class B(A):
    def method(self):
        print("Method in B")

class C(A):
    def method(self):
        print("Method in C")

class D(B, C):
    pass

print(D.mro())  # Outputs the MRO list of D
Python

Output

[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
Python

In the example above, the MRO for class D is [D, B, C, A, object]. When a method is called on an instance of class D, Python will first check class D, then B, followed by C, and finally A. The last class in the MRO is the object, which is the base class for all Python classes.

Diamond Inheritance

Diamond inheritance is a problem that arises in multiple inheritance in object-oriented programming (OOP). It occurs when a class inherits from two or more classes that have a common ancestor, resulting in a “diamond” shape in the inheritance hierarchy.

        A
       / \
      B   C
       \ /
        D
Python

Problems with Diamond Inheritance

The diamond problem arises because:

  • Ambiguity in Method Resolution: If both B and C define the same method (e.g., method()), and D calls super().method(), it’s unclear which method should be called — the one from B or the one from C?
  • Duplicate Calls to the Ancestor: If both B and C call a method from A, D could end up calling A’s method multiple times, which might not be desirable.

How Python Handles the Diamond Problem

Python handles diamond inheritance using the C3 Linearization (or C3 superclass linearization) algorithm. The C3 linearization ensures that each class is called only once, and it determines the order in which classes should be searched for methods and attributes.

Order for Diamond inheritance

[D, B, C, A, object]
Python

The Role of super() in MRO

The super() function is often used in Python to call methods from a parent class. In multiple inheritance scenarios, the super() function relies on the MRO to determine which class’s method to call. This allows developers to avoid explicitly calling methods from specific base classes, enabling a more flexible and maintainable approach to method resolution.

Consider the following example:

class A:
    def method(self,via):
        print(f"A's method {via}")

class B(A):
    def method(self,via):
        print(f"B's method {via}")
        super().method("from b")

class C(A):
    def method(self,via):
        print(f"C's method {via}")
        super().method("from c")

class D(B, C):
    pass

d = D()
print(D.mro())  
d.method("from d")
Python

Output

[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

B's method from d
C's method from b
A's method from c
Python

Interesting points

  • A is not called twice
  • C’s method called from b
  • A’s method called from c

To come up with output use MRO logic. Refer

In this case, the MRO of class D is [D, B, C, A, object]. When d.method() is called, Python first checks class D (which does not have a method), then moves to class B and calls its method. Inside B.method(), super().method() is used, which resolves to calling C.method() because of the MRO. Finally, C.method() calls super().method(), which resolves to A.method().

Examples

Example 1

class A:
    def __init__(self):
        print("A initialized")

class B(A):
    def __init__(self):
        super().__init__()
        print("B initialized")

class C(A):
    def __init__(self):
        super().__init__()
        print("C initialized")

class D(B, C):
    def __init__(self):
        super().__init__()
        print("D initialized")

print(D.mro())  
obj = D()
Python

Output

[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

A initialized
C initialized
B initialized
D initialized
Python

Example 2

      X   Y
      |   |
      B   C
       \ /
        D
Python

Order will [D,B,X,C,Y]

class Y:
    def __init__(self):
        print("Y initialized")


class X:
    def __init__(self):
       print("X initialized")


class B(X):
    def __init__(self):
        super().__init__()
        print("B initialized")

class C(Y):
    def __init__(self):
        super().__init__()
        print("C initialized")

class D(B,C):
    def __init__(self):
        super().__init__()
        print("D initialized")

print(D.mro())  
obj = D()
Python

Output

[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.X'>, <class '__main__.C'>, <class '__main__.Y'>, <class 'object'>]

X initialized
B initialized
D initialized
Python

Points

  • C is skipped
  • Y is skipped

Start with D -> will call B –> B will call X-> Print x-> control back to B -> Print B -> Back to D -> Print to D

Is MRO only for inheritance?

Yes, the Method Resolution Order (MRO) primarily applies to inheritance in Python, specifically in which classes are searched for methods, attributes, and other members in a multiple inheritance hierarchy. The MRO determines the sequence of classes that Python will check when a method or attribute is accessed. It is crucial to understand how method calls are resolved, particularly in cases of multiple inheritance, where more than one class might define the same method or attribute.

Understanding the Importance of MRO

The MRO ensures that Python can consistently and unambiguously resolve method calls, especially when dealing with complex inheritance hierarchies. Here are a few reasons why understanding MRO is important:

  • Predictable Method Resolution: In cases of multiple inheritance, MRO ensures that Python always follows the same path when resolving methods, avoiding conflicts and confusion.
  • Using super() Effectively: The MRO helps in leveraging the super() function to call parent methods without explicitly specifying the parent class, which is especially useful in cooperative multiple inheritance.
  • Avoiding Ambiguity: Without MRO, method resolution could lead to ambiguous results if two base classes define methods with the same name, potentially leading to bugs in your code.

Conclusion

The Method Resolution Order (MRO) in Python is a powerful mechanism that ensures method lookup follows a clear, predictable path, especially when using multiple inheritance. Understanding how Python resolves method calls through the MRO is essential for writing maintainable and bug-free object-oriented code. Whether you’re using super() to call parent methods or dealing with complex inheritance chains, MRO provides the foundation for Python’s object-oriented behaviour. By leveraging MRO effectively, you can avoid method resolution conflicts and create more flexible and robust applications.

Resources

Leave a Comment