Zum Inhalt springen

5. Vererbung

Zu Zen-Modus wechseln

Programme wachsen, und mit ihnen die Zahl der Klassen. Dabei entsteht regelmäßig dieselbe Situation: Mehrere Klassen sind sich ähnlich, aber nicht gleich. Eine Mediendatenbank verwaltet Bilder, Videos und Audiodateien; alle drei haben einen Dateinamen, eine Dateigröße und ein Aufnahmedatum, aber nur das Video hat eine Framerate und nur das Bild eine Auflösung in Megapixel. Wer für jede dieser Klassen den gemeinsamen Teil erneut schreibt, erzeugt Duplikate, und Duplikate sind mögliche Wartungsfehler in der Zukunft: Jede Korrektur muss an mehreren Stellen nachgezogen werden, und irgendwann wird eine vergessen.

Vererbung ist das Sprachmittel gegen dieses Problem: Gemeinsamkeiten werden in einer Basisklasse gesammelt, die Besonderheiten stehen in Subklassen, die alles aus der Basisklasse übernehmen. Vererbung ist zugleich mehr als ein Werkzeug gegen Code-Duplikate; sie ist die Grundlage für Polymorphismus (Kapitel 6) und damit für erweiterbare Programmarchitekturen.

Zwei Klassen einer Mediendatenbank, ohne Vererbung:

class ImageAsset:
def __init__(self, filename, size_bytes, width, height):
self.filename = filename
self.size_bytes = size_bytes
self.width = width
self.height = height
def size_megabytes(self):
return self.size_bytes / (1024 * 1024)
def megapixels(self):
return self.width * self.height / 1_000_000
class VideoAsset:
def __init__(self, filename, size_bytes, duration_seconds, fps):
self.filename = filename
self.size_bytes = size_bytes
self.duration_seconds = duration_seconds
self.fps = fps
def size_megabytes(self):
return self.size_bytes / (1024 * 1024)
def frame_count(self):
return int(self.duration_seconds * self.fps)

filename, size_bytes und size_megabytes() sind exakt doppelt vorhanden. Bei einer dritten Klasse AudioAsset wäre der Block ein drittes Mal fällig. Genau dieses Muster, gleiche Attribute und gleiche Methoden in mehreren Klassen, ist das Signal für Vererbung.

Die Gemeinsamkeiten wandern in eine Basisklasse MediaAsset; die Subklassen geben sie in Klammern hinter ihrem Namen an:

class MediaAsset:
"""Base class for all files in the media database."""
def __init__(self, filename, size_bytes):
self.filename = filename
self.size_bytes = size_bytes
def size_megabytes(self):
return self.size_bytes / (1024 * 1024)
def __str__(self):
return f"{self.filename} ({self.size_megabytes():.1f} MB)"
class ImageAsset(MediaAsset):
def __init__(self, filename, size_bytes, width, height):
super().__init__(filename, size_bytes)
self.width = width
self.height = height
def megapixels(self):
return self.width * self.height / 1_000_000
class VideoAsset(MediaAsset):
def __init__(self, filename, size_bytes, duration_seconds, fps):
super().__init__(filename, size_bytes)
self.duration_seconds = duration_seconds
self.fps = fps
def frame_count(self):
return int(self.duration_seconds * self.fps)

Die Verwendung:

photo = ImageAsset("sunset.jpg", 4_200_000, 6000, 4000)
clip = VideoAsset("intro.mp4", 380_000_000, 90, 25)
print(photo.size_megabytes()) # 4.0 -> inherited from MediaAsset
print(photo.megapixels()) # 24.0 -> defined in ImageAsset
print(clip) # intro.mp4 (362.4 MB) -> __str__ from MediaAsset

Die zentralen Begriffe:

  • MediaAsset ist die Basisklasse (auch Superklasse oder Elternklasse). ImageAsset und VideoAsset sind Subklassen (abgeleitete Klassen, Kindklassen).
  • Eine Subklasse erbt alle Attribute und Methoden der Basisklasse: photo.size_megabytes() funktioniert, obwohl ImageAsset diese Methode nirgends definiert. Python sucht die Methode zuerst in der Klasse des Objekts und steigt dann die Hierarchie hinauf.
  • Vererbung drückt eine “ist-ein”-Beziehung aus: Ein Bild ist ein Medien-Asset, ein Video ist ein Medien-Asset. Dieser Satz ist der wichtigste Test dafür, ob Vererbung angemessen ist; dazu später mehr.

Definiert eine Subklasse ein eigenes __init__, ersetzt es das geerbte vollständig. Die Initialisierung der Basisattribute muss deshalb explizit angestoßen werden: super().__init__(filename, size_bytes) ruft den Konstruktor der Basisklasse auf. super() liefert dabei einen Verweis auf die Basisklasse, über den deren Methoden erreichbar sind, auch wenn die Subklasse sie überschrieben hat.

Der häufigste Fehler in diesem Kapitel ist ein vergessenes super().__init__(...): Die Basisattribute existieren dann nicht, und der erste Zugriff darauf endet mit AttributeError: 'ImageAsset' object has no attribute 'filename'. Wer diese Meldung bei einer Subklasse sieht, prüft zuerst den Konstruktor.

In UML-Klassendiagrammen wird Vererbung als Pfeil mit hohler Dreiecksspitze dargestellt, der von der Subklasse zur Basisklasse zeigt:

classDiagram
    MediaAsset <|-- ImageAsset
    MediaAsset <|-- VideoAsset
    MediaAsset <|-- AudioAsset

    class MediaAsset {
        +filename
        +size_bytes
        +size_megabytes()
    }
    class ImageAsset {
        +width
        +height
        +megapixels()
    }
    class VideoAsset {
        +duration_seconds
        +fps
        +frame_count()
    }
    class AudioAsset {
        +duration_seconds
        +sample_rate
    }

Lesart: Die Pfeilspitze zeigt auf die allgemeinere Klasse (“ist ein”). In den Subklassen werden nur die zusätzlichen Attribute und Methoden eingetragen; die geerbten stehen bereits in der Basisklasse. Diese Notation wird in Kapitel 12 vertieft.

Eine Subklasse kann eine geerbte Methode neu definieren (überschreiben, englisch override), wenn das geerbte Verhalten nicht passt. Beispiel: Die Kurzbeschreibung eines Videos soll die Dauer enthalten:

class VideoAsset(MediaAsset):
# ... constructor as above ...
def __str__(self):
return (f"{self.filename} ({self.size_megabytes():.1f} MB, "
f"{self.duration_seconds} s @ {self.fps} fps)")
>>> print(photo)
sunset.jpg (4.0 MB) # __str__ from MediaAsset
>>> print(clip)
intro.mp4 (362.4 MB, 90 s @ 25 fps) # overridden in VideoAsset

Beim Aufruf entscheidet die Klasse des Objekts, welche Fassung läuft. Python sucht von der konkreten Klasse aufwärts und nimmt den ersten Treffer.

Oft soll die überschriebene Methode das geerbte Verhalten nicht ersetzen, sondern ergänzen. Auch dafür ist super() zuständig:

class VideoAsset(MediaAsset):
def __str__(self):
base_description = super().__str__()
return f"{base_description}, {self.duration_seconds} s @ {self.fps} fps"

Die Subklasse ruft die Basisfassung auf und erweitert deren Ergebnis. Dieses Muster hält die Hierarchie wartbar: Ändert sich die Basisdarstellung, ändern sich alle Subklassen automatisch mit. Kopieren des Basiscodes in die Subklasse wäre wieder genau das Duplikat, das die Vererbung beseitigen sollte.

Die Funktion isinstance(obj, cls) prüft, ob ein Objekt zu einer Klasse gehört, einschließlich ihrer Subklassen:

>>> isinstance(photo, ImageAsset)
True
>>> isinstance(photo, MediaAsset)
True # an image IS a media asset
>>> isinstance(photo, VideoAsset)
False
>>> type(photo)
<class '__main__.ImageAsset'> # type() reports the exact class only

Dass isinstance die Vererbung berücksichtigt, ist kein Nebeneffekt, sondern der Kern der “ist-ein”-Beziehung: Überall, wo ein MediaAsset erwartet wird, ist ein ImageAsset zulässig. Alle Klassen in Python erben übrigens automatisch von der eingebauten Wurzelklasse object; daher stammen die Standardimplementierungen von __str__ und __repr__ aus dem letzten Kapitel.

Ein Hinweis zur Verwendung: Lange isinstance-Ketten in der Art “wenn Bild, dann dies; wenn Video, dann das” sind meist ein Zeichen, dass eigentlich eine überschriebene Methode fehlt. Das folgende Kapitel über Polymorphismus behandelt genau diesen Punkt.

Vererbung lässt sich schachteln. Eine Fotoagentur unterscheidet etwa Rohdaten-Bilder und bearbeitete Bilder:

class RawImageAsset(ImageAsset):
def __init__(self, filename, size_bytes, width, height, camera_model):
super().__init__(filename, size_bytes, width, height)
self.camera_model = camera_model

RawImageAsset erbt von ImageAsset, das von MediaAsset erbt; ein RawImageAsset-Objekt hat damit die Attribute und Methoden aller drei Ebenen. super().__init__(...) reicht die Initialisierung dabei Stufe für Stufe nach oben durch.

Technisch sind beliebig tiefe Hierarchien möglich, praktisch sind sie mit Vorsicht zu genießen. Ab etwa drei Ebenen wird schwer nachvollziehbar, welches Verhalten ein Objekt von welcher Ebene bezieht; Änderungen an einer Basisklasse wirken auf alle Nachkommen, auch auf die, an die niemand gedacht hat. Flache Hierarchien mit einer klaren Basisklasse und einer Ebene von Subklassen decken die meisten realen Fälle ab. Python erlaubt darüber hinaus Mehrfachvererbung (eine Klasse mit mehreren Basisklassen); sie bringt eigene Komplexität mit und bleibt in diesem Jahr außen vor.

Vererbung ist das am häufigsten falsch eingesetzte Werkzeug der Objektorientierung. Zwei Prüffragen trennen die berechtigten von den unberechtigten Fällen.

Vererbung modelliert ausschließlich “ist-ein”-Beziehungen. Für “hat-ein”- oder “besteht-aus”-Beziehungen ist Komposition zuständig: Das eine Objekt hält das andere als Attribut.

# WRONG: a project is not a video asset
class Project(VideoAsset):
...
# RIGHT: a project HAS assets
class Project:
def __init__(self, name):
self.name = name
self.assets = [] # composition: a list of MediaAsset objects
def add(self, asset):
self.assets.append(asset)
def total_size_megabytes(self):
return sum(asset.size_megabytes() for asset in self.assets)

Im Klassendiagramm wird Komposition durch eine Linie mit gefüllter Raute dargestellt; die Raute steht am Ganzen (dem Project), das die Teile hält. Die Angaben "1" und "*" sind Multiplizitäten: Ein Projekt hält beliebig viele Assets.

classDiagram
    Project "1" *-- "*" MediaAsset

    class Project {
        +name
        +assets
        +add(asset)
        +total_size_megabytes()
    }
    class MediaAsset {
        +filename
        +size_bytes
        +size_megabytes()
    }

Der Unterschied zur Vererbung ist auf einen Blick erkennbar: Vererbung (“ist-ein”) verwendet die hohle Dreiecksspitze, Komposition (“hat-ein”/“besteht-aus”) die gefüllte Raute.

Der Sprachtest ist zuverlässig: “Ein Projekt ist ein Video” klingt falsch, “ein Projekt hat Videos” klingt richtig. Wer erbt, nur um an eine praktische Methode wie size_megabytes() zu kommen, missbraucht die Hierarchie; die Methode gehört dann in ein gehaltenes Objekt oder in eine Funktion.

Prüffrage 2: Verhält sich die Subklasse überall wie die Basisklasse?

Abschnitt betitelt „Prüffrage 2: Verhält sich die Subklasse überall wie die Basisklasse?“

Eine Subklasse muss überall einsetzbar sein, wo die Basisklasse erwartet wird, ohne dass sich das Programm falsch verhält. Dieses Prinzip heißt nach seiner Autorin Barbara Liskov das Liskovsche Substitutionsprinzip.

Das Standardbeispiel für einen Verstoß: Square erbt von Rectangle. Mathematisch ist jedes Quadrat ein Rechteck, aber im Code kippt die Beziehung. Wer bei einem Rechteck die Breite auf 10 und die Höhe auf 5 setzt, erwartet die Fläche 50. Ein Quadrat kann diese Erwartung nicht erfüllen: Es muss beim Setzen der Breite heimlich die Höhe mitziehen, sonst wäre es kein Quadrat mehr. Code, der mit Rechtecken korrekt arbeitet, liefert mit einem untergeschobenen Quadrat falsche Ergebnisse. Die Vererbungsbeziehung ist damit unbrauchbar, so plausibel sie klingt.

Merksatz für den Alltag: Eine Subklasse darf Erwartungen an die Basisklasse nicht enttäuschen. Sie darf Verhalten ergänzen und präzisieren, aber keine zugesicherten Eigenschaften brechen, keine gültigen Eingaben ablehnen, die die Basisklasse akzeptiert, und keine Methoden mit “das kann ich nicht” beantworten, die die Basisklasse verspricht.

Entwurfsleitfaden: von den Anforderungen zur Hierarchie

Abschnitt betitelt „Entwurfsleitfaden: von den Anforderungen zur Hierarchie“
  1. Klassenkandidaten sammeln. Aus der Aufgabenstellung die Dinge herausschreiben, die verwaltet werden sollen (typischerweise Substantive), samt ihren Attributen und Operationen.

  2. Gemeinsamkeiten markieren. Attribute und Methoden, die in mehreren Kandidaten identisch auftauchen, sind Kandidaten für eine Basisklasse.

  3. “ist-ein” prüfen. Für jede geplante Ableitung den Satz bilden: “Ein X ist ein Y.” Klingt er unpassend, Komposition erwägen.

  4. Substitution prüfen. Für jede Subklasse fragen: Kann sie überall einspringen, wo die Basisklasse erwartet wird, ohne Erwartungen zu brechen?

  5. Diagramm zeichnen, dann implementieren. Erst das Klassendiagramm mit Vererbungspfeilen skizzieren, dann in Code übersetzen. Der Weg zurück (Code lesen, Diagramm ableiten) ist ebenso Prüfungsstoff.

Als Übungsmaßstab: Für eine Aufgabe wie die Mediathek (Aufgabe 09) sollte der Entwurf vor der ersten Codezeile stehen und aus einem Diagramm mit drei bis fünf Klassen bestehen. Entwürfe, die beim Programmieren “mitwachsen”, enden erfahrungsgemäß in Hierarchien, die keine Prüffrage bestehen.

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Erklären: das Konzept der Vererbung, die Begriffe Basisklasse und Subklasse sowie die “ist-ein”-Beziehung erklären.
  • Anwenden: Klassenhierarchien mit gemeinsamer Basisklasse entwerfen und in Python implementieren (class Sub(Base), super().__init__).
  • Anwenden: geerbte Methoden überschreiben und mit super() erweitern statt duplizieren.
  • Analysieren: mit isinstance Typbeziehungen prüfen und die Wirkung der Methodensuche entlang der Hierarchie nachvollziehen.
  • Beurteilen: anhand der “ist-ein”-Prüfung und des Substitutionsprinzips begründen, wann Vererbung angemessen ist und wann Komposition vorzuziehen ist.
  • Darstellen: Vererbungsbeziehungen in UML-Klassendiagrammen notieren und lesen.
  • Aufgabe 09 - Mediathek
  • Aufgabe 10 - Spielfiguren
  • Aufgabe 11 - Komposition statt Vererbung