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 A
s 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
.