Python FAQ: Top Questions
42. What is the MRO (Method Resolution Order) in Python? How does it work?
The **Method Resolution Order (MRO)** in Python is the order in which Python searches for methods and attributes in a class hierarchy, particularly in the case of multiple inheritance. When you call a method on an object, Python needs a consistent way to determine which implementation of that method to use if it's defined in multiple superclasses.
Python's MRO ensures a deterministic and consistent lookup order. It is crucial for understanding how inheritance works in complex scenarios.
How MRO Works (C3 Linearization Algorithm):
Python 2.3 introduced the **C3 linearization algorithm** (also known as the C3 MRO) for all new-style classes (which are all classes in Python 3). This algorithm ensures:
- **Preservation of Local Order:** A class always precedes its parents. (e.g., if `B` inherits from `A`, then `B` will always appear before `A` in the MRO).
- **Monotonicity:** If a class `X` precedes class `Y` in the MRO of class `C`, then `X` will also precede `Y` in the MRO of any subclass of `C`. This means the order of base classes is preserved.
- **Coherent Super Calls:** Ensures that `super()` calls correctly dispatch to the "next" method in the MRO, even in complex inheritance scenarios, facilitating cooperative multiple inheritance.
The C3 algorithm is somewhat complex to explain in detail, but its essence is to construct a linear order of classes that satisfies the local precedence order (left-to-right in the `class` definition) and the inheritance graph (child before parent).
How to Check MRO:
You can inspect a class's MRO using a few methods:
- **`ClassName.__mro__`**: Returns a tuple of classes in MRO order.
- **`ClassName.mro()`**: Returns a list of classes in MRO order.
- **`help(ClassName)`**: Provides comprehensive documentation, including the MRO.
Understanding `super()` and MRO:
The `super()` function in Python relies directly on the MRO. When you call `super().method()`, it doesn't just call the method of the immediate parent. Instead, it calls the `method` defined in the *next class in the MRO* after the current class. This allows for cooperative multiple inheritance, where each class in the hierarchy can contribute to the behavior of a method.
This is distinct from simply calling `ParentClass.method(self, ...)`, which always calls a specific parent's method and can lead to issues (e.g., diamond problem) in multiple inheritance.
Why is MRO important?
- **Predictable Behavior:** Ensures that method and attribute lookups are consistent and predictable, regardless of the complexity of the inheritance hierarchy.
- **Solving the "Diamond Problem":** In multiple inheritance, if a class inherits from two classes that both inherit from a common ancestor, there's a "diamond" shape in the inheritance graph. The MRO resolves ambiguities about which ancestor's method to call.
- **Cooperative Multiple Inheritance:** Enables classes to work together effectively in a multiple inheritance scenario, allowing each class to contribute to a shared method implementation by correctly chaining calls using `super()`.
- **Debugging:** Understanding the MRO is essential for debugging issues in complex class hierarchies, as it tells you exactly where Python expects to find attributes and methods.
In practice, while the C3 algorithm is intricate, Python handles it automatically. As a developer, your main interaction with MRO is through understanding its principles for designing class hierarchies and using `super()` correctly.
# --- Example 1: Simple Inheritance MRO ---
print("--- Simple Inheritance MRO ---")
class A:
def method(self):
print("Method from A")
class B(A):
def method(self):
print("Method from B")
class C(A):
def method(self):
print("Method from C")
class D(B, C): # Inherits from B first, then C (left-to-right precedence)
def method(self):
print("Method from D")
print("MRO for D:", D.__mro__)
# Expected: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
d = D()
d.method() # Will call D's method
print("\n--- Illustrating MRO with super() ---")
class E:
def greet(self):
print("Hello from E")
class F(E):
def greet(self):
print("Hello from F")
super().greet() # Calls the next greet() in MRO (E's greet)
class G(E):
def greet(self):
print("Hello from G")
super().greet() # Calls the next greet() in MRO (E's greet)
class H(F, G): # Inherits from F, then G
def greet(self):
print("Hello from H")
super().greet() # Calls F's greet
print("MRO for H:", H.__mro__)
# Expected: (<class '__main__.H'>, <class '__main__.F'>, <class '__main__.G'>, <class '__main__.E'>, <class 'object'>)
h = H()
h.greet()
# Output:
# Hello from H
# Hello from F
# Hello from G
# Hello from E
# --- Example 2: The "Diamond Problem" and MRO Resolution ---
print("\n--- Diamond Problem MRO ---")
class Base:
def identify(self):
print("I am Base")
class Left(Base):
def identify(self):
print("I am Left")
super().identify() # Calls Base.identify()
class Right(Base):
def identify(self):
print("I am Right")
super().identify() # Calls Base.identify()
class Diamond(Left, Right): # Inherits from Left, then Right
def identify(self):
print("I am Diamond")
super().identify() # Calls Left.identify()
print("MRO for Diamond:", Diamond.__mro__)
# Expected: (<class '__main__.Diamond'>, <class '__main__.Left'>, <class '__main__.Right'>, <class '__main__.Base'>, <class 'object'>)
diamond_obj = Diamond()
diamond_obj.identify()
# Output:
# I am Diamond
# I am Left
# I am Right
# I am Base
# Note: Base.identify() is called only once, correctly resolved by MRO and super().
# --- Example 3: MRO for Disallowed Inheritance (TypeError) ---
print("\n--- Disallowed Inheritance (MRO TypeError) ---")
class X: pass
class Y: pass
# This scenario often leads to MRO problems in older Python versions
# or if you try to force an illogical inheritance pattern.
# For example, if you had a class inheriting from X, and another from Y,
# and then try to inherit from both Y and X in an order that violates C3.
# In C3, this specific simple example often resolves, but complex ones might not.
# Example of a problematic case (often called 'zig-zag' or 'cross' inheritance):
# class A(X, Y): pass
# class B(Y, X): pass
# class C(A, B): pass # This would cause MRO error because X and Y order is inconsistent.
# Let's try a simpler conflict
class Top: pass
class M1(Top): pass
class M2(Top): pass
class M3(M1, M2): pass
class M4(M2, M1): pass # M1 and M2 are already ordered relative to Top
try:
class Final(M3, M4): # Here, M3 wants (M1, M2), M4 wants (M2, M1)
pass
except TypeError as e:
print(f"Caught expected MRO TypeError: {e}")
# This demonstrates that Python's MRO algorithm will raise an error if a consistent
# linearization cannot be found, preventing ambiguous behavior.
Explanation of the Example Code:
-
**Simple Inheritance MRO:**
- `D(B, C)` means `D` inherits from `B` first, then `C`. The MRO for `D` shows `D` first, then `B` (because `B` is a direct parent and appears first in the inheritance list), then `C`, then `A` (common ancestor), and finally `object`.
- The `H` example demonstrates `super()`. When `h.greet()` is called, `H.greet()` runs. Inside `H.greet()`, `super().greet()` calls `F.greet()` (the next in MRO). `F.greet()` then calls `G.greet()`, and finally `G.greet()` calls `E.greet()`. This illustrates the cooperative chaining behavior enabled by `super()` and MRO.
-
**The "Diamond Problem" MRO:**
- This classic problem arises when `Diamond` inherits from `Left` and `Right`, both of which inherit from `Base`. Without a consistent MRO, it's ambiguous whether `Base.identify()` should be called once or twice, and in what order relative to `Left` and `Right`.
- Python's C3 MRO resolves this: `Diamond` -> `Left` -> `Right` -> `Base` -> `object`. Notice that `Base` appears only once, at the very end of its lineage, ensuring it's called exactly once.
- When `diamond_obj.identify()` is called, `Diamond.identify()` runs, then `super().identify()` correctly dispatches to `Left.identify()`, which then calls `Right.identify()`, which finally calls `Base.identify()`. The output clearly shows each method being called in the defined MRO order, and `Base` is called only once.
-
**MRO for Disallowed Inheritance (TypeError):**
- This example showcases a scenario where the MRO algorithm cannot construct a consistent linear order.
- `M3` defines its parents as `(M1, M2)`, and `M4` defines its parents as `(M2, M1)`. When `Final` tries to inherit from both `M3` and `M4`, the MRO algorithm finds a conflict: `M3` demands that `M1` come before `M2`, while `M4` demands the opposite.
- Because a consistent linear order cannot be found, Python raises a `TypeError` during class definition, preventing an ambiguous or incorrect lookup behavior at runtime. This demonstrates the robustness of the C3 MRO in preventing problematic inheritance hierarchies.
These examples illustrate the MRO's role in establishing a predictable method lookup order, solving the diamond problem, and enabling cooperative multiple inheritance with `super()`.