Skip to content
academia.sh

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 Wrapper are the shared rig’s defined form; the placement of capabilities has not been changed.
  • CD17 — The four measured capabilities are source, format, stamp, and signature; 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.
  • Document carries 0 of the four capabilities in its own body, and the answer comes from three separate classes; Wrapper writes all 4 of the same four capabilities in its own body.
  • format’s body is written in Record, but since the call inside it resolves through self, 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 TypeError at 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 same LeftLayer body goes to Layer in one instance and to RightLayer in 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.

Start typing to search.

↑↓ Esc navigate · open · close