5.22. Inheritance¶
Inheritance lets a class extend another class – start with all of its methods and attributes, then add or override what is different. The original is called the base (or parent); the extension is called the subclass.
Square reuses every method on Shape and overrides
the ones it needs to specialise.¶
5.22.1. Defining a subclass¶
The base class goes in the parentheses on the class line:
class Shape:
def __init__(self, name):
self.name = name
def describe(self):
return self.name + " with area " + str(self.area())
def area(self):
return 0
class Square(Shape):
def __init__(self, side):
super().__init__("square")
self.side = side
def area(self):
return self.side * self.side
s = Square(4)
print(s.describe())
Output:
square with area 16
Square inherits describe from Shape unchanged and
overrides area to return a real value. The base’s
describe still works because it calls self.area() –
which resolves to the override at run time.
5.22.2. super()¶
super() returns a proxy that lets a method call the
base-class version of a method. The most common use is calling
the base __init__ from a subclass __init__ so the base
gets to initialise its own attributes:
class Square(Shape):
def __init__(self, side):
super().__init__("square") # run Shape.__init__
self.side = side
Forgetting super().__init__ is a common source of bugs:
attributes the base would have set never get set, and the
inherited methods that rely on them crash later.
5.22.2.1. The implicit base class¶
Every class implicitly inherits from object, the root
of Python’s type hierarchy. Methods like __repr__, __eq__, and the
machinery behind attribute access all come from there even when
a class declares no base. Writing class Foo(object):
explicitly and writing class Foo: are equivalent in modern
Python; the latter is the conventional form.
5.22.3. When inheritance helps – and when it does not¶
Use inheritance when one class is genuinely a more specific
kind of another, sharing most of the behaviour. The classic
test is the “is-a” check: a Square is a Shape.
When two classes just happen to share a few helpers, inheritance is overkill. Reach for composition instead: have one class hold an instance of the other as an attribute and use it through that attribute. The shape is “has-a”, not “is-a”:
class Logger:
def log(self, msg):
print("[log]", msg)
# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
def run(self):
self.log("starting")
# composition: Worker HAS-A Logger
class WorkerComposes:
def __init__(self):
self.logger = Logger()
def run(self):
self.logger.log("starting")
Both work. The composing version is usually better because it:
Keeps interfaces small.
WorkerComposesexposes onlyrunandlogger.WorkerInheritsalso exposeslog– callers can writeworker.log(...)directly, whether or not that was intended.Decouples lifetimes. The logger can be swapped, shared between workers, or constructed differently per worker, all without touching the
Workerclass. A subclass is bolted to its base.Avoids method-name collisions. Two bases that both define
runclash when a class tries to inherit from both; two attributes never clash.
A practical rule of thumb:
Use inheritance when the subclass really is a kind of the base and every method on the base makes sense on it too –
Squareis aShape;MemoryErroris anException.Use composition for everything else – when you just want the behaviour of another class, not its identity. Most real code uses composition far more than it uses inheritance.