I just got my head around the “method resolution order” (MRO) rules for new classes in Python. If you aren’t aware, in Python 2.2 (now nine years old, from 2001) they introduced so-called “new-style classes”. Basically, to create a new-style class, you inherit from the built-in class object or any other new-style class. If you don’t inherit from anything, you create an old-style class. (And in Python 3 onwards, there are no more old-style classes, so you no longer need to write (object) on every class.)
I’ve known for a while that they’re “good” and I should use them. Aside from the immediate benefits of being able to inherit from built-in types and them being unified with the type system (type(myobject) == MyClass and type(MyClass) == type, rather than type(myobject) == instance and type(MyClass) == classobj), I knew there was something “fixed” about the method resolution order — the set of rules for deciding which version of an overridden method to call in a complex multiple-inheritance hierarchy.
Now having read The Python 2.3 Method Resolution Order, I understand it, but that document is a little wordy, so here is the simplest example I can think of. I’ll build the classic diamond problem, out of old-style classes:
class D: # Note: Old-style def f(self): return "D.f()" class B(D): pass class C(D): def f(self): return "C.f()" class A(B, C): pass >>> a = A() >>> a.f() 'D.f()'
The f method of B returns “D.f()” (due to inheriting from D and not overriding). The f method of C returns “C.f()”. Obviously since A inherits both B and C, there is some contention about which to use. Under the old rules (depth first left to right), it chooses the version in B, even though C is closer to A and in some sense “more authoritative”. It’s certainly a tricky question: it seems like it should have chosen the version in C, because after all, C has explicitly overridden the version in D and B hasn’t. But on the other hand, why should B behave differently depending on whether it inherited a definition or supplied its own?
In any event, the new-style rules favour the “most authoritative” version:
class D(object): # Note: New-style def f(self): return "D.f()" class B(D): pass class C(D): def f(self): return "C.f()" class A(B, C): pass >>> a = A() >>> a.f() 'C.f()'
Because C overrides D, it “wins”. The actual logic here is to go up depth-first but stop when you find a class which is subclassed later on. For example, when looking for the definition of f to use, it tries to go up A – B – D, but stops when it realises that D is being subclassed by a class which we haven’t considered yet (C). So it considers C first before looking at D, and finds a “stronger” definition there.
Most importantly, this new rule ensures monotonicity, and that is explained in the article.
[Edit: I’m pretty sure when the article talks about “Python 2.2 classes”, it does not refer to “old-style classes”, but rather the initial implementation of new-style classes in Python 2.2, which was flawed, and fixed in Python 2.3. So it is no longer possible to try out the “flawed” behaviour mentioned there without going back and installing version 2.2.]