Method Resolution Order (Mro) in New-Style Classes

Method Resolution Order (MRO) in new-style classes?

The crucial difference between resolution order for legacy vs new-style classes comes when the same ancestor class occurs more than once in the "naive", depth-first approach -- e.g., consider a "diamond inheritance" case:

>>> class A: x = 'a'
...
>>> class B(A): pass
...
>>> class C(A): x = 'c'
...
>>> class D(B, C): pass
...
>>> D.x
'a'

here, legacy-style, the resolution order is D - B - A - C - A : so when looking up D.x, A is the first base in resolution order to solve it, thereby hiding the definition in C. While:

>>> class A(object): x = 'a'
...
>>> class B(A): pass
...
>>> class C(A): x = 'c'
...
>>> class D(B, C): pass
...
>>> D.x
'c'
>>>

here, new-style, the order is:

>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
<class '__main__.A'>, <type 'object'>)

with A forced to come in resolution order only once and after all of its subclasses, so that overrides (i.e., C's override of member x) actually work sensibly.

It's one of the reasons that old-style classes should be avoided: multiple inheritance with "diamond-like" patterns just doesn't work sensibly with them, while it does with new-style.

How Method Resolution Order (MRO) is working in this Python code

The documentation has a great explanation of how super() works:

super([type[, object-or-type]])

Return a proxy object that delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class.

The object-or-type determines the method resolution order to be searched. The search starts from the class right after the type.

For example, if __mro__ of object-or-type is D -> B -> C -> A -> object and the value of type is B, then super() searches C -> A -> object.

super() is equivalent to the expression super(__class__, self) where __class__ is an class object within whose method the super() is called. For example, super() within grandchild.callingparent(self) is essentially super(grandchild, self). And super() inside child1.callchildform1(self) function is super(child1, self).

MRO for grandchild is (<grandchild>, <child1>, <child2>, <parent>, <object>). Therefore, according to the above excerpt from documentation, when super().form1() is called in child1.callchildform1(), which is equivalent to super(child1, self), the search for the form1 method starts from the class right after the child1 in the MRO sequence and the first matching class with form1 method is child2.

This occurs due to that you are using the diamond inheritance structure and the principles that underlie the MRO:

with multiple inheritance hierarchies, the construction of the linearization is more cumbersome, since it is more difficult to construct a linearization that respects local precedence ordering and monotonicity.

When designing such hierarchies, it is need to follow the approach of cooperative inheritance, an explanation can be found, in this already classic article.

Method resolution order in python

If you stop to think about it, it is just the intuitive way of working. This article, which by now just looks like an archaeological find, is still the authoritative description and reasoning on Python's method resolution order algorithm.

But despite the tecnical details in there, what happen in your two examples are:

In the first one, D,B,C,A, the path through B indicates that A's attribute should be used. But As attribute itself is shadowed by the attribute in C - that is, the declaration in C overrides the attr declared in A. Therefore it is the one used.

In the second hierarchy, D,B,C,A,E, B comming first than C, again indicates that A.attr should be used. This time, however, A's own attribute had not been shadowed by another class in the hierarchy - rather C.attr comes from another "lineage" - so the language picks the first it encounters.

That is the "plain english description" of what is going on. The authoritative article linked above lays the formal rules for that :

the linearization of [a class] C is the sum of C plus the merge of the
linearizations of the parents and the list of the parents.
...
[given class C(B1, ..., BN):], take the head of the first list, i.e L[B1][0] [linearization (aka mro) of Base B1 up to Object - the head is B1 -]; if this head is not in
the tail of any of the other lists [linearization lists for the other bases] , then add it to the linearization
of C and remove it from the lists in the merge, otherwise look at the
head of the next list and take it, if it is a good head. Then repeat
the operation until all the class are removed or it is impossible to
find good heads. In this case, it is impossible to construct the
merge, Python 2.3 [and subsequente versions] will refuse to create the class C and will raise an
exception.

Bringing in your second example, you have
D(B, C) - the linearizations for B and C are: [B, A, object] and [C, E, object] and the linearization of D starts by taking "B", checking it is not
on the tail of any other lists (and it is not on [C, E, object]), then B is taken. The remaining lists are [A, object] and [C, E, object] - the algorithm then picks A it is not in the other list, then A is appended to the mro of D. It then picks object. It is on the other list. So the algorithm leaves the first list intact, and takes C, E and finally object, for D, B, A, C, E, object linearization.

In your first example, the linearization of both bases are [B, A, object] and [C, A, object] when the algorithm checks for A, it is be on the tail of the second list - so, C is picked first than A from the second list - the final lineariation is D, B, C, A, object.

How to customize the method resolution order (mro) of Python?

Your best bet is probably to have Third explicitly use the Second implementation:

class Third(First, Second):
set = Second.set

although the fact that you're asking this at all is a warning sign that you may have picked a bad class hierarchy.

How to check MRO (Method Resolution Order) in Python 2.7?

There is no explicit resolution order for old-style classes; instead, method resolution depends on the transitive closure of the set of parent classes.

From The Python 2.3 Method Resolution Order (emphasis mine):

First of all, let me point out that what I am going to say only applies to the new style classes introduced in Python 2.2: classic classes maintain their old method resolution order, depth first and then left to right.

The resolution order, if exposed at all as explicit data, would have been an implementation detail, not part of the defined interface by the language.

Does the MRO of this class hierarchy is the same for old-style and new-style?

The thing is, whether in Python 2.2, 2.3, or later, these are all new-style classes. There are 3 method resolution schemes here, not 2:

  1. Classic class method resolution.
  2. Python 2.2 new-style class method resolution.
  3. Python 2.3+ C3 linearization.

Schemes 2 and 3 would produce the same MRO for the given example.

If you want to see the (now almost completely irrelevant) Python 2.2 method resolution scheme, the most complete documentation is probably a blog post from Guido's old Python history blog. There's also an old archived draft of documentation for Python 2.2, which mostly describes it but fails to mention a special case.

A consistent Method Resolution Order couldn't be created (MRO error)

childB is attempting to inherit from Base twice, once through childA and once directly. Fix by removing the multiple inheritance on childB.

class Base(object):
def __init__(self):
print ("Base")

class childA(Base):
def __init__(self):
print ('Child A')
Base.__init__(self)

class childB(childA):
def __init__(self):
print ('Child B')
super(childB, self).__init__()

b=childB()


Related Topics



Leave a reply



Submit