Explain Python’s method resolution order (MRO).
Python's Method Resolution Order (MRO) is the sequence in which Python searches for a method in a class hierarchy, particularly when dealing with multiple inheritance. It determines the order in which base classes are searched to find a method or attribute, ensuring a consistent and predictable behavior.
What is MRO?
MRO dictates the order in which Python traverses the inheritance tree to find the implementation of a method or attribute. When an instance of a class calls a method, Python looks for that method in the instance's class, then in its parent classes, and so on, following the MRO. This ensures that methods are called from the correct base class in complex inheritance scenarios.
Why is MRO Necessary?
MRO becomes crucial in situations involving multiple inheritance, where a class inherits from two or more parent classes. Without a defined order, it would be ambiguous which parent's method should be called if multiple parents implement a method with the same name. This is particularly evident in the 'diamond problem,' where a class inherits from two classes that both derive from a common base class. MRO resolves such ambiguities by providing a deterministic lookup order.
How MRO Works: C3 Linearization Algorithm
Since Python 2.3, MRO is determined using the C3 linearization algorithm. C3 ensures two main properties: monotonicity (if a method is found in class A, it won't be found later in a class that's a superclass of A in the MRO) and local precedence order (a class always precedes its parents, and in multiple inheritance, a class precedes its listed base classes from left to right).
- Local Precedence Order: A class is always checked before its parents.
- Monotonicity: The MRO of a class must respect the MRO of its superclasses. If C1 comes before C2 in the MRO of one class, it must also come before C2 in the MRO of any subclass that inherits from both C1 and C2.
- Head Rule: The first element of the MRO of a class
CisCitself, followed by the merge of the MROs of its parents and a list of its parents.
Example
Consider a classic 'diamond' inheritance pattern:
class A:
def greet(self):
print("Hello from A")
class B(A):
def greet(self):
print("Hello from B")
class C(A):
def greet(self):
print("Hello from C")
class D(B, C):
# No greet method here
pass
class E(C, B):
# No greet method here
pass
print(D.__mro__)
# Expected output: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
print(E.mro())
# Expected output: [<class '__main__.E'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
In the D(B, C) example, Python's C3 MRO prioritizes B over C because B was listed first in D's inheritance list. After checking D itself, it checks B, then C, and finally their common ancestor A, before reaching the ultimate base class object. For E(C, B), the MRO is similarly determined by the order of base classes in its definition, leading to C being checked before B.
Key Takeaways
- MRO resolves the order of method lookup in class hierarchies, especially with multiple inheritance.
- Python uses the C3 linearization algorithm to determine MRO, ensuring consistency and predictability.
- You can inspect the MRO of any class using the
__mro__attribute (a tuple) or themro()method (a list). - The order of base classes in the class definition significantly impacts the resulting MRO.