Advanced OOP Design
|
This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Two objects can be related by composition ("has-a") or inheritance ("is-a"). Python also allows a class to inherit from several parents at once, which makes the order methods are looked up in — the Method Resolution Order — a first-class thing to understand, not an implementation detail.
Composition ("has-a") vs. Inheritance ("is-a")
Inheritance models "a Car is a Vehicle`"; composition models "a `Car has a `Engine`". Prefer
composition whenever the relationship is really about reuse of behavior rather than a genuine
sub-type — it keeps the two classes independently testable and avoids coupling them through a shared
base. The tutorial’s
classes chapter covers both attribute lookup and the
inheritance mechanics this choice rests on.
# Inheritance ("is-a"): Car really is a kind of Vehicle -- it shares Vehicle's interface.
class Vehicle:
def __init__(self, speed):
self.speed = speed
def describe(self):
return f"moving at {self.speed} km/h"
class Car(Vehicle):
pass
# Composition ("has-a"): Car has an Engine -- it delegates to it instead of extending it.
class Engine:
def start(self):
return "engine started"
class Car2:
def __init__(self, engine):
self.engine = engine # composed, not inherited
def start(self):
return self.engine.start()
print(Car(80).describe()) # moving at 80 km/h
print(Car2(Engine()).start()) # engine started
If Car2 later needs a different engine (electric instead of combustion), swapping the composed
object requires no change to Car2 itself — with inheritance, swapping a base class is far more
invasive. This is the core argument for "favor composition over inheritance" as a default.
Mixins, Multiple Inheritance, and the MRO
Python lets a class list several bases: class D(B, C):. A mixin is a small base class meant only to
be combined this way — it supplies one focused piece of behavior and is never instantiated on its own.
Multiple inheritance is documented in the tutorial’s
Multiple Inheritance section.
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class LoggingMixin:
def log(self, msg):
print(f"[{type(self).__name__}] {msg}")
class User(JSONMixin, LoggingMixin):
def __init__(self, name):
self.name = name
u = User("ada")
u.log("created") # [User] created
print(u.to_json()) # {"name": "ada"}
The Method Resolution Order (mro)
When several bases could supply the same attribute, Python resolves it using the Method Resolution
Order — a single, deterministic ordering of the class and all its ancestors, computed by the C3
linearization algorithm. Inspect it with Cls.mro or Cls.mro(). The algorithm itself is explained
in depth in The Python 2.3 Method Resolution Order, which still
describes the C3 algorithm CPython uses today.
Take the classic diamond: B and C both extend A, and D extends both B and C.
class A:
def who(self):
return "A"
class B(A):
def who(self):
return "B"
class C(A):
def who(self):
return "C"
class D(B, C):
pass
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
>>> D().who()
'B'
C3 guarantees that a class always appears before its own bases, and that the declared base order
(B before C in class D(B, C)) is preserved — so D is checked first, then B, then C, then
their shared ancestor A only once, and finally object.
super() in a Multi-Parent Hierarchy
super() does not mean "my direct parent" — it means "the next class in the MRO after the current
one". Each cooperative method calls super().init(…) and lets the MRO decide who runs next,
so every base in the chain gets a chance to run exactly once. This is documented under
the built-in super() reference.
class Base:
def __init__(self):
print("Base.__init__")
class Left(Base):
def __init__(self):
print("Left.__init__")
super().__init__() # goes to whatever is next in *this instance's* MRO
class Right(Base):
def __init__(self):
print("Right.__init__")
super().__init__()
class Bottom(Left, Right):
def __init__(self):
print("Bottom.__init__")
super().__init__()
>>> Bottom()
Bottom.__init__
Left.__init__
Right.__init__
Base.__init__
<__main__.Bottom object at 0x...>
Left.init’s `super() calls Right.init, not Base.init directly — because in
Bottom’s MRO, `Right sits between Left and Base. Without cooperative super() calls,
Base.init would run twice or not at all.
Delegation as an Alternative to Inheritance
Delegation wraps an internal instance and forwards calls to it explicitly, instead of inheriting its
interface. It gives full control over which operations are exposed, at the cost of writing the
forwarding methods by hand. The standard library’s own
collections.UserDict is built
this way — it wraps a real dict in a .data attribute rather than subclassing dict directly.
class ReadOnlyList:
"""Wraps a list, exposing only read operations -- no append/remove/etc."""
def __init__(self, items):
self._items = list(items) # composed instance, not inherited
def __getitem__(self, index):
return self._items[index] # forward to the wrapped list
def __len__(self):
return len(self._items) # forward to the wrapped list
def __iter__(self):
return iter(self._items) # forward to the wrapped list
r = ReadOnlyList([1, 2, 3])
print(len(r), list(r), r[0]) # 3 [1, 2, 3] 1
# r.append(4) # AttributeError: not forwarded, and that's the point
Delegation trades the automatic interface inheritance gives you for explicit control over the surface area — useful whenever a class should reuse an object’s behavior without exposing everything that object can do.
See Also
-
Classes and Objects — the class and instance fundamentals this page builds on.
-
Operator Overloading — giving custom classes operator support via dunder methods.