Python Method Resolution Order (MRO): A Comprehensive Deep Dive
In Python, the Method Resolution Order (MRO) is the mechanism that determines the order in which base classes are searched when looking for a method or attribute in a class hierarchy. This is especially critical in the context of inheritance, particularly with multiple inheritance, where ambiguity could arise. Python uses the C3 linearization algorithm to compute the MRO, ensuring a consistent and predictable resolution path. In this blog, we’ll explore what MRO is, how it works, practical examples, its features, and its significance in Python’s object-oriented programming.
What Is Method Resolution Order (MRO)?
The Method Resolution Order is a sequence of classes that Python follows to resolve a method or attribute call on an object. It defines the lookup path from the instance’s class through its superclasses up to the root object class.
Key Concepts
- Inheritance Hierarchy : MRO applies when a class inherits from one or more base classes.
- C3 Linearization : The algorithm Python uses to compute a linear order, balancing depth-first and left-to-right traversal.
- Consistency : Ensures predictable behavior, even in complex multiple inheritance scenarios.
Example
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C):
pass
d = D()
print(d.method()) # Output: B
print(D.__mro__) # Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
How MRO Works in Python
Defining the MRO
- The MRO is calculated when a class is defined and stored in the __mro__ attribute (a tuple of classes).
- Python uses the C3 linearization algorithm, which:
- Starts with the class itself.
- Considers the order of base classes (left-to-right).
- Respects the inheritance hierarchy (depth-first where possible).
- Avoids duplicates and ensures each class appears only once.
Basic Structure
class X:
def who(self):
return "X"
class Y(X):
def who(self):
return "Y"
class Z(Y):
pass
z = Z()
print(z.who()) # Output: Y
print(Z.__mro__) # Output: (<class 'Z'>, <class 'Y'>, <class 'X'>, <class 'object'>)
Accessing the MRO
- __mro__ : Class attribute showing the resolution order.
- mro() : Class method returning the same as a list.
Example
print(Z.mro()) # Output: [<class 'Z'>, <class 'Y'>, <class 'X'>, <class 'object'>]
MRO in Action
- When a method is called (e.g., obj.method()), Python:
- Looks at the instance’s class.
- Follows the MRO to find the first class defining the method.
- Executes that method.
Features of MRO
1. Single Inheritance
In simple hierarchies, MRO is straightforward:
class A:
def info(self):
return "A"
class B(A):
pass
b = B()
print(b.info()) # Output: A
print(B.__mro__) # Output: (<class 'B'>, <class 'A'>, <class 'object'>)
2. Multiple Inheritance
MRO resolves ambiguity in complex hierarchies:
class A:
def greet(self):
return "Hello from A"
class B(A):
def greet(self):
return "Hello from B"
class C(A):
pass
class D(B, C):
pass
d = D()
print(d.greet()) # Output: Hello from B
print(D.__mro__) # Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
3. Diamond Problem
MRO handles the classic diamond problem predictably:
class A:
def method(self):
return "A"
class B(A):
pass
class C(A):
def method(self):
return "C"
class D(B, C):
pass
d = D()
print(d.method()) # Output: C
print(D.__mro__) # Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
- C precedes A because it’s listed later in D(B, C).
4. Using super()
super() follows the MRO to call the next method in the chain:
class A:
def step(self):
return "A step"
class B(A):
def step(self):
return f"B step, {super().step()}"
class C(B):
pass
c = C()
print(c.step()) # Output: B step, A step
print(C.__mro__) # Output: (<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>)
Practical Examples
Example 1: Simple Hierarchy
class Vehicle:
def move(self):
return "Moving"
class Car(Vehicle):
def move(self):
return "Driving"
c = Car()
print(c.move()) # Output: Driving
print(Car.__mro__) # Output: (<class 'Car'>, <class 'Vehicle'>, <class 'object'>)
Example 2: Multiple Inheritance
class Flyer:
def travel(self):
return "Flying"
class Driver:
def travel(self):
return "Driving"
class Hybrid(Flyer, Driver):
pass
h = Hybrid()
print(h.travel()) # Output: Flying
print(Hybrid.__mro__) # Output: (<class 'Hybrid'>, <class 'Flyer'>, <class 'Driver'>, <class 'object'>)
Example 3: Diamond with Super
class Base:
def process(self):
return "Base"
class Left(Base):
def process(self):
return f"Left, {super().process()}"
class Right(Base):
def process(self):
return f"Right, {super().process()}"
class Bottom(Left, Right):
def process(self):
return f"Bottom, {super().process()}"
b = Bottom()
print(b.process()) # Output: Bottom, Left, Right, Base
print(Bottom.__mro__) # Output: (<class 'Bottom'>, <class 'Left'>, <class 'Right'>, <class 'Base'>, <class 'object'>)
Example 4: Mixin Pattern
class Loggable:
def log(self):
return "Logging"
class Worker:
def work(self):
return "Working"
class Robot(Worker, Loggable):
def work(self):
return f"{super().work()}, {self.log()}"
r = Robot()
print(r.work()) # Output: Working, Logging
print(Robot.__mro__) # Output: (<class 'Robot'>, <class 'Worker'>, <class 'Loggable'>, <class 'object'>)
Performance Implications
Overhead
- Lookup Cost : MRO introduces a small overhead for method resolution, proportional to hierarchy depth.
- Cached : MRO is computed once at class definition, not per call.
Benchmarking
import time
class A:
def m(self):
pass
class B(A):
pass
class C(B):
pass
c = C()
start = time.time()
for _ in range(1000000):
c.m()
print(time.time() - start) # Minimal overhead
Memory
- Lightweight : MRO is stored as a tuple in __mro__, negligible memory impact.
MRO vs. Other Constructs
- Single Inheritance : MRO is trivial, just a linear chain.
- Multiple Inheritance : MRO shines, resolving complexity other languages might restrict.
- Manual Resolution : Without MRO, developers would need explicit calls, reducing flexibility.
Practical Use Cases
- Framework Design :
class BaseHandler: def handle(self): return "Base" class CustomHandler(BaseHandler): def handle(self): return "Custom"
- Mixin Classes :
class Printable: def print(self): return "Printing" class Device(Printable): pass
- Plugin Systems :
class Plugin: def execute(self): pass class BackupPlugin(Plugin): def execute(self): return "Backup"
- Superclass Coordination :
class A: def init(self): return "A init" class B(A): def init(self): return f"B, {super().init()}"
Edge Cases and Gotchas
1. Invalid MRO
class A: pass
class B(A): pass
class C(A, B): pass # TypeError: Cannot create a consistent method resolution order
- MRO fails if the hierarchy is inconsistent.
2. Super() Behavior
class X:
def m(self):
return "X"
class Y(X):
def m(self):
return super().m()
y = Y()
print(y.m()) # Output: X
3. Overriding in Middle
class A:
def x(self):
return "A"
class B(A):
def x(self):
return "B"
class C(B):
pass
c = C()
print(c.x()) # Output: B
4. Diamond Ambiguity
class Top:
def m(self):
return "Top"
class Left(Top): pass
class Right(Top):
def m(self):
return "Right"
class Bottom(Left, Right): pass
b = Bottom()
print(b.m()) # Output: Right
Conclusion
Python’s Method Resolution Order, powered by the C3 linearization algorithm, is a robust system for navigating class hierarchies, especially in multiple inheritance scenarios. By defining a clear, predictable path through __mro__, it resolves method lookups dynamically and consistently. From simple single inheritance to complex diamond patterns, MRO ensures flexibility and clarity in object-oriented design. Understanding its mechanics—how it orders classes and interacts with super()—empowers you to leverage inheritance effectively, building scalable, maintainable Python code.