2.22. Vererbung

Vererbung lässt eine Klasse eine andere Klasse erweitern – man beginnt mit all ihren Methoden und Attributen und fügt dann hinzu oder überschreibt, was anders ist. Das Original wird Basis (oder Elternklasse) genannt; die Erweiterung wird Unterklasse genannt.

Eine Basisklasse Shape, die nach unten auf eine Unterklasse Square zeigt; Square überschreibt eine Methode (area) und erbt den Rest.

Square verwendet jede Methode von Shape wieder und überschreibt diejenigen, die es spezialisieren muss.

2.22.1. Eine Unterklasse definieren

Die Basisklasse steht in den Klammern in der class-Zeile:

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())

Ausgabe:

square with area 16

Square erbt describe unverändert von Shape und überschreibt area, um einen echten Wert zurückzugeben. Das describe der Basis funktioniert weiterhin, weil es self.area() aufruft – was zur Laufzeit auf die Überschreibung aufgelöst wird.

2.22.2. super()

super() gibt einen Proxy zurück, der es einer Methode ermöglicht, die Basisklassen-Version einer Methode aufzurufen. Die häufigste Verwendung ist der Aufruf des Basis-__init__ aus dem __init__ einer Unterklasse, damit die Basis ihre eigenen Attribute initialisieren kann:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

Das Vergessen von super().__init__ ist eine häufige Fehlerquelle: Attribute, die die Basis gesetzt hätte, werden nie gesetzt, und die geerbten Methoden, die sich darauf verlassen, stürzen später ab.

2.22.2.1. Die implizite Basisklasse

Jede Klasse erbt implizit von object, der Wurzel von Pythons Typhierarchie. Methoden wie __repr__, __eq__ und die Mechanik hinter dem Attributzugriff stammen alle von dort, selbst wenn eine Klasse keine Basis deklariert. class Foo(object): explizit zu schreiben und class Foo: zu schreiben ist im modernen Python äquivalent; Letzteres ist die übliche Form.

2.22.3. Wann Vererbung hilft – und wann nicht

Verwenden Sie Vererbung, wenn eine Klasse wirklich eine spezifischere Art einer anderen ist und den Großteil des Verhaltens teilt. Der klassische Test ist die „ist-ein“-Prüfung: ein Square ist ein Shape.

Wenn zwei Klassen nur zufällig ein paar Helfer teilen, ist Vererbung überzogen. Greifen Sie stattdessen zur Komposition: Lassen Sie eine Klasse eine Instanz der anderen als Attribut halten und nutzen Sie sie über dieses Attribut. Die Form ist „hat-ein“, nicht „ist-ein“:

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")

Beides funktioniert. Die komponierende Variante ist in der Regel besser, weil sie:

  • Schnittstellen klein hält. WorkerComposes legt nur run und logger offen. WorkerInherits legt zusätzlich log offen – Aufrufer können direkt worker.log(...) schreiben, ob das beabsichtigt war oder nicht.

  • Lebensdauern entkoppelt. Der Logger kann ausgetauscht, zwischen Workern geteilt oder pro Worker unterschiedlich konstruiert werden, ohne die Worker-Klasse anzufassen. Eine Unterklasse ist fest mit ihrer Basis verschraubt.

  • Methodennamenskollisionen vermeidet. Zwei Basen, die beide run definieren, kollidieren, wenn eine Klasse versucht, von beiden zu erben; zwei Attribute kollidieren nie.

Eine praktische Faustregel:

  • Verwenden Sie Vererbung, wenn die Unterklasse wirklich eine Art der Basis ist und jede Methode der Basis auch auf sie sinnvoll anwendbar ist – Square ist ein Shape; MemoryError ist eine Exception.

  • Verwenden Sie Komposition für alles andere – wenn Sie nur das Verhalten einer anderen Klasse wollen, nicht ihre Identität. Der meiste reale Code verwendet Komposition weitaus häufiger als Vererbung.