Search code examples
pythonmethod-resolution-orderpython-mro

Method resolution order in Python


I am new to Python. I am using Python 2.7. I was going through method order resolution with a small snippet as follows:

class A(object):
    attr = 'A'

class B(A):
    pass

class C(A):
    attr = 'C'

class D(B,C):
    pass

x = D()
print x.attr

The order of resolution is x, D, B, C, A and hence the output would be C. Going by the above example, I made a small change to the code.

class A(object):
    attr = 'A'

class E(object):
    attr = 'E'

class B(A):
    pass

class C(E):
    attr = 'C'

class D(B,C):
    pass

x = D()
print x.attr

Going by my previous example, I expected the order to be x, D, B, C, A, E. To my surprise the output was "A". Hence I got confused with the order of resolution in new-style class. When was B's parent accessed before C class?


Solution

  • 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 technical 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 coming 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 subsequent 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 linearization is D, B, C, A, object.