Lesson 02 / 11
Inheritance and Method Resolution Order
The body of four capabilities spreads across three separate classes in inheritance, and `Document` writes 0 of them in its own body; `Wrapper` writes all 4 in composition. The `format` method, whose body is written in `Record`, prints `<Signed>`.
Contents
The previous lesson showed that in a single-class tree, the answer always comes from the
same place: lookup checks the instance’s dict, moves to the class if it fails there, and
stops. The last measurement had already drawn the line — a class method whose body is
written in Formatter, called through SubFormatter, bound SubFormatter to cls. Where
the body sits and where the call is answered had already come apart.
This lesson enlarges that split and pays off the course’s second claim. The same four capabilities will be given in two separate designs: one an inheritance tree, the other composition. Both designs produce the same results. The question is not the result: which class is each capability’s body written in? The question suits measurement, because its answer comes from reading code, not opinion — a name either exists in a class’s dict or it does not.
Same Four Capabilities, Two Designs
The inheritance side is made of four classes. Record carries two capabilities: source,
and format, which calls it. Timed derives from it and adds stamp. Signed also derives
from it, adds signature, and rewrites source. Document derives from both and its
body is empty.
The composition side is a single class. Wrapper derives from nothing; it holds two objects
inside itself and writes all four capabilities in its own body, passing each call through to
the object inside it.
Which class a capability’s body is written in is told by the method resolution order
(MRO). Every class carries such an order: a list, computed once at definition time, of
which classes a lookup walks and in what sequence. The answering_class function walks this
list and returns the first class carrying the name.
Document‘s empty body is not a gap, it is the measurement itself. That body declares
exactly one thing: which classes it derives from, and in what order. The class’s entire
behavior is born from this single declaration, and when the declaration changes, so does the
behavior — reorder the bases and the class answering source changes; add one line to
Document’s body and the answer shifts there. Composition has no such declaration;
Wrapper’s behavior is read only from its own body.
The measurement’s assumptions:
- CD16 — The four-class tree and
Wrapperare the shared rig’s defined form; the placement of capabilities has not been changed. - CD17 — The four measured capabilities are
source,format,stamp, andsignature; the measurement looks at the class the body is written in, not the call’s result. - CD18 — The composition column is not a constant — it would come out the same even read
from
Wrapper’s own dict: all four names are written in that body. - CD19 — When the resolution order is printed, the list’s last item (the common base of all classes) is trimmed; what is measured is the rig’s own four classes.
"""Who answers the call: inheritance and composition side by side.""" class Record: def source(self): return "Record" def format(self): return f"<{self.source()}>" class Timed(Record): def stamp(self): return "time" class Signed(Record): def source(self): return "Signed" def signature(self): return "signature" class Document(Timed, Signed): pass class Wrapper: """An object giving the same capability via composition: who answers is written explicitly.""" def __init__(self): self.timed = Timed() self.signed = Signed() def source(self): return self.signed.source() def stamp(self): return self.timed.stamp() def signature(self): return self.signed.signature() def format(self): return f"<{self.source()}>" def answering_class(cls, name): """Which class provides an attribute: the first one found during resolution.""" for s in cls.__mro__: if name in s.__dict__: return s.__name__ return None CAPABILITIES = ("source", "format", "stamp", "signature") k = {name: answering_class(Document, name) for name in CAPABILITIES} b = {name: "Wrapper" for name in CAPABILITIES} print(f"{'capability':<10s} {'answering in inheritance':>25s} {'answering in composition':>25s}") for name in CAPABILITIES: print(f"{name:<10s} {k[name]:>25s} {b[name]:>25s}") print() print("resolution order: " + " -> ".join(s.__name__ for s in Document.__mro__[:-1])) print(f"Document().format() -> {Document().format()} | Wrapper().format() -> " f"{Wrapper().format()}") print(f"in inheritance, capability with body written in Document: " f"{sum(1 for a in CAPABILITIES if k[a] == 'Document')}/{len(CAPABILITIES)}; " f"in composition: {sum(1 for a in CAPABILITIES if b[a] == 'Wrapper')}/{len(CAPABILITIES)}")
capability answering in inheritance answering in composition source Signed Wrapper format Record Wrapper stamp Timed Wrapper signature Signed Wrapper resolution order: Document -> Timed -> Signed -> Record Document().format() -> <Signed> | Wrapper().format() -> <Signed> in inheritance, capability with body written in Document: 0/4; in composition: 4/4
The last line is the claim itself. Document carries 0 of the four capabilities in its
own body; Wrapper carries all 4. Reading Document’s body shows exactly one thing —
two base names; what the object can do is not written in that body. Reading Wrapper’s
body shows all four capabilities and who each call is passed to.
The left column shows the answer spread across three separate classes: source comes from
Signed, format from Record, stamp from Timed. source is written in two classes at
once — both Record and Signed — but since Signed comes first in resolution order, it
gives the answer. The body in Record is still there and is never called.
The subtler point sits in the second row. format’s body is written in Record, and
that body writes self.source(). Since the body sits inside Record, it might seem it would
call the source there; it does not. The call is made through self, and self is a
Document instance — the lookup runs in Document’s order and stops at Signed. The result
comes out <Signed>. Reading a body does not tell you what it will produce; that is told
by the resolution order of the object at call time. Wrapper gives the same result, but
there the destination of the pass-through is written inside the body itself.
The Programming Paradigms course established preferring composition over inheritance as a design principle, alongside the fragile base class problem; that discussion is not repeated here. What is measured here is not the principle’s justification but its measurable counterpart: where the body is written. 0/4 against 4/4 is the same behavior’s two different readability costs.
The two numbers point in opposite directions, and the choice is made exactly here. Adding a
fifth capability costs 0 lines in inheritance — a method written on Timed shows up on
Document automatically. In composition it costs 1 line, a pass-through written on
Wrapper. In exchange, answering “who answers this call” costs reading four bodies and their
order in inheritance, one body in composition. Inheritance shortens the write and lengthens
the read; composition does the reverse.
The split has an object side too. A Document instance is one object; all four classes’
bodies operate on a single dict, and any state Record holds sits there once. A
Wrapper instance, by contrast, is three objects — itself and the two built in
__init__ — and state from Record is held separately in each. Both designs answer the
same calls, but store state in a different number of places: sharing is unavoidable in
inheritance, and happens in composition only if written explicitly.
How the Order Is Built
The order Document -> Timed -> Signed -> Record is not arbitrary; it is produced by two
rules: every class comes before its own bases, and bases are kept in the order they are
written in the class body. Record sits last because it is the base of both Timed and
Signed, and both have to come before it.
These two rules cannot always be satisfied together. When they cannot, the class cannot be
built, and the error is raised not while an instance is built but while the class
statement itself runs.
- CD20 — Four base orders are built from the same three classes; the built class itself is dropped from the output, only the order after it is printed.
- CD21 — For an order that fails to build, the exception’s name is recorded with an exclamation prefix.
- CD22 — The classes are the previous block’s definitions; the tree is unchanged.
def try_building(bases): """Can a class be built with the given base order?""" try: s = type("Trial", bases, {}) except TypeError as e: return f"!{type(e).__name__}" return " -> ".join(t.__name__ for t in s.__mro__[1:-1]) print(f"{'base order':<22s} {'order after the built class':<34s}") for bases in ((Timed, Signed), (Signed, Timed), (Timed, Record), (Record, Timed)): name = "(" + ", ".join(t.__name__ for t in bases) + ")" print(f"{name:<22s} {try_building(bases):<34s}") print() print(f"Record's position in Document's order: " f"{Document.__mro__.index(Record)} (last of the four classes)") print(f"Timed's direct base: " f"{', '.join(t.__name__ for t in Timed.__bases__)}") print(f"class right after Timed in Document's order: " f"{Document.__mro__[Document.__mro__.index(Timed) + 1].__name__}")
base order order after the built class (Timed, Signed) Timed -> Signed -> Record (Signed, Timed) Signed -> Timed -> Record (Timed, Record) Timed -> Record (Record, Timed) !TypeError Record's position in Document's order: 3 (last of the four classes) Timed's direct base: Record class right after Timed in Document's order: Signed
Three of four orderings build, one fails. The first two lines show base-writing order
directly decides the result: swap the places and the class answering source changes too.
The fourth line draws the boundary — with Record written first and its derivative Timed
second, the rule “every class comes before its bases” cannot be satisfied, and the class
cannot be built at all. This is not a runtime error; it is raised at definition time. The
order is computed once per class and stored on it, not rebuilt on every access. Lookup cost,
for this reason, depends not on the tree’s shape but on how far into the order a name is
found.
The last three lines close the question of where the first table’s order came from. Record
is Timed’s direct base; despite that, the class coming right after Timed in
Document‘s order is not Record, it is Signed. The order does not follow a single
branch straight up the tree; once it passes a class it does not jump to that branch’s base —
it pushes the common base to the very end.
The reason sits in the first measurement. If the order were
Document -> Timed -> Record -> Signed, source lookup would stop at Record, and the body
Signed writes would never be found — a method one sibling rewrites lost because of the
other sibling’s base. Pushing the common base to the end prevents this: the base is only
asked once all its derivatives have been tried. The order’s two rules are therefore not a
style choice, they are the condition for multiple inheritance to make sense at all.
super() Goes to the Order, Not the Ancestor
The most visible consequence of this is the super() call. Its name reads like an “ancestor
class” call, but that is not what it does: super() goes to the class right after the
one the body is written in, in the calling object’s resolution order. The body stays the
same, the order changes, the destination changes.
- CD23 — Four new classes are built for this measurement; the rig’s tree is untouched.
- CD24 — All four classes write the same name, and its body is a single line: put its own
name on the list, and continue the chain via
super(). The base class ends the chain. - CD25 — What is measured is not the call’s result but the sequence of bodies that ran; the number is the body count.
- CD26 — The second table reads
LeftLayer’s body through two separate instances; the body is one, the instances are two different classes.
class Layer: def label(self): return ["Layer"] class LeftLayer(Layer): def label(self): return ["LeftLayer"] + super().label() class RightLayer(Layer): def label(self): return ["RightLayer"] + super().label() class Combined(LeftLayer, RightLayer): def label(self): return ["Combined"] + super().label() def next_class(cls, body): """Where does the super() call written in class 'body' go?""" m = cls.__mro__ return m[m.index(body) + 1].__name__ print(f"{'instance class':<14s} {'bodies run':>10s} chain") for s in (LeftLayer, Combined): chain = s().label() print(f"{s.__name__:<14s} {len(chain):>10d} {' -> '.join(chain)}") print() print(f"{'body written in':<16s} {'instance class':<16s} super() goes to") for s in (LeftLayer, Combined): print(f"{'LeftLayer':<16s} {s.__name__:<16s} {next_class(s, LeftLayer)}")
instance class bodies run chain LeftLayer 2 LeftLayer -> Layer Combined 4 Combined -> LeftLayer -> RightLayer -> Layer body written in instance class super() goes to LeftLayer LeftLayer Layer LeftLayer Combined RightLayer
LeftLayer’s body is identical in both measurements and a single line. On a LeftLayer
instance, the super() call in that line goes to Layer, and the chain ends at 2
bodies. On a Combined instance, the same line goes to RightLayer — a class that is
not LeftLayer’s base and whose name does not even appear in its body — and the chain ends
at 4 bodies.
RightLayer never being skipped is the whole point of this arrangement: in multiple
inheritance, every class’s body runs exactly once, and the common base runs once at
the very end. For the chain to work, every link has to call super(); once one class stops
calling it, every body after it silently drops out. Setup methods follow the same chain:
when super().__init__(...) is not written inside __init__, the intermediate classes’
setup bodies never run at all, and the gap only surfaces once a missing field is read.
The chain’s second condition is signatures. Since a body does not know which class super()
will land on, every body sharing a name has to be written with parameters that accept each
other. Whoever wrote LeftLayer’s body may never have seen RightLayer; even so, the two
run side by side in the same call. Composition carries no such implicit contract —
Wrapper.__init__ builds the two inner objects explicitly, by name and by argument, and the
destination is written in the body, not in an order.
The design-side conclusion follows from this. Reading a body correctly is not enough to know
the class it sits in; which object the call came from also has to be known. Composition
has no such unknown: the call inside Wrapper.format’s body goes to self.signed, and that
name is written inside the body itself. Inheritance gives a short write and hides who
answers; composition demands a longer write and writes who answers.
What is hidden here is not unreadable, though. Resolution order is not a guess, it is a list
sitting on the class and readable at any time; the answering_class function is three lines
walking that list. A capability’s body does not have to be traced by eye through the class
tree — the order can be asked directly. Inheritance’s cost is not that information
disappears, it is that reading the call requires asking for it separately.
Summary
- Which class answers a capability is decided by the method resolution order: if a name is written in more than one class, whichever comes first in the order wins, and the others are never called.
Documentcarries 0 of the four capabilities in its own body, and the answer comes from three separate classes;Wrapperwrites all 4 of the same four capabilities in its own body.format’s body is written inRecord, but since the call inside it resolves throughself, the result comes out<Signed>: reading a body does not tell you its result.- Resolution order is built by two rules — every class before its bases, bases in the order
they are written. Three of four base orderings build; the one that violates the
rule falls with a
TypeErrorat definition time. super()does not go to the base of the class the body is written in — it goes to whatever comes after that class in the instance’s order: the sameLeftLayerbody goes toLayerin one instance and toRightLayerin another, and the chain runs 2 versus 4 bodies.
Next Step
Up to here, every name was assumed visible from outside: Document’s instance source and
the signed object Wrapper holds inside itself could both be read the same way. But
signed is Wrapper‘s internal arrangement; code touching it from outside binds itself to a
detail Wrapper might change tomorrow. This is another place the two designs part ways: in
inheritance every base class’s name passes to the derivative as is; in composition, what is
exposed outward is written name by name in the body. The next lesson measures how Python
declares this distinction: does an underscore in front of a name actually block access,
what does a double underscore change, and if it does not block access, does the declaration
have any measurable effect at all?
To keep your progress and take notes, Log in
My notes
Log in to take notes.