Abschnitt 14 · Lektion 5
Vererbung
In Mias Laden gibt es Flaschen, und auf jeder liegt Pfand. An der Kasse kostet das Mineralwasser
0,49 Euro plus 0,25 Euro Pfand. Das Pfand gehört nicht zum Preis, es kommt ja zurück. Ein
Pfandartikel ist also ein Artikel mit einem Wert mehr und einer Rechnung, die anders geht. Die ganze
Klasse dafür zu kopieren hieße, __init__ und __str__ zweimal zu pflegen.
Eine Klasse erbt von einer anderen
class Artikel:
def __init__(self, name, preis, bestand=0):
self.name = name
self.preis = preis
self.bestand = bestand
def kassenpreis(self):
return self.preis
def __str__(self):
return f"{self.name}: {self.kassenpreis():.2f} Euro"
class Pfandartikel(Artikel):
def __init__(self, name, preis, pfand, bestand=0):
super().__init__(name, preis, bestand)
self.pfand = pfand
def kassenpreis(self):
return super().kassenpreis() + self.pfand
korb = [
Artikel("Brot", 2.49),
Pfandartikel("Mineralwasser", 0.49, 0.25),
Artikel("Butter", 2.29),
]
for artikel in korb:
print(artikel)
print(f"Zusammen: {sum([artikel.kassenpreis() for artikel in korb]):.2f} Euro")
print(isinstance(korb[1], Artikel), isinstance(korb[0], Pfandartikel)) class Pfandartikel(Artikel): legt eine neue Klasse an, die alles erbt, was Artikel kann. Das
nennt man Vererbung (inheritance). Artikel ist die Elternklasse (parent class), Pfandartikel die
Unterklasse (subclass). In Pfandartikel steht nur, was anders ist.
Anders ist zuerst __init__, weil es einen Parameter mehr gibt.
super().__init__(name, preis, bestand) ruft das __init__ der Elternklasse auf, und das setzt
Name, Preis und Bestand wie bei jedem Artikel. Danach kommt das Eigene, self.pfand = pfand.
Anders ist außerdem kassenpreis. Eine Methode mit demselben Namen wie in der Elternklasse
überschreibt (override) die geerbte. Mit super().kassenpreis() holt man sich trotzdem deren
Ergebnis dazu und muss die Rechnung nicht wiederholen.
__str__ steht nur in Artikel und gilt trotzdem für das Wasser. Darin wird self.kassenpreis()
aufgerufen, und beim Wasser ist self ein Pfandartikel. Also läuft dessen Fassung, und heraus
kommen 0,74 Euro. Die Schleife unten muss nicht wissen, welche Art Artikel gerade dran ist. Jedes
Objekt bringt seine eigene Rechnung mit.
isinstance(objekt, Klasse) fragt, ob ein Objekt zu einer Klasse gehört. Ein Pfandartikel ist auch
ein Artikel, ein Artikel aber kein Pfandartikel.
Ohne super()
class Artikel:
def __init__(self, name, preis, bestand=0):
self.name = name
self.preis = preis
self.bestand = bestand
def kassenpreis(self):
return self.preis
def __str__(self):
return f"{self.name}: {self.kassenpreis():.2f} Euro"
class Pfandartikel(Artikel):
def __init__(self, name, preis, pfand, bestand=0):
self.pfand = pfand
def kassenpreis(self):
return super().kassenpreis() + self.pfand
wasser = Pfandartikel("Mineralwasser", 0.49, 0.25)
print(wasser) Diesmal fehlt super().__init__(...) im __init__ der Unterklasse. Mit einem eigenen __init__
ersetzt die Unterklasse das der Elternklasse komplett. Das alte läuft nur, wenn man es ausdrücklich
aufruft. Hier setzt also niemand den Namen, und print(wasser) endet mit
AttributeError: 'Pfandartikel' object has no attribute 'name'.
Der Traceback zeigt dabei in __str__ von Artikel, auf die Stelle, an der nach dem Namen gefragt
wird. Der Fehler steckt aber im __init__ von Pfandartikel, wo der Name hätte gesetzt werden
sollen. Wieder einmal meldet sich ein Fehler weit weg von seiner Ursache. Wer das einmal gesehen
hat, sucht beim nächsten AttributeError in einer Unterklasse zuerst nach dem super().
Wann man es lassen sollte
class Artikel:
def __init__(self, name, preis, bestand=0, pfand=0):
self.name = name
self.preis = preis
self.bestand = bestand
self.pfand = pfand
def kassenpreis(self):
return self.preis + self.pfand
def __str__(self):
return f"{self.name}: {self.kassenpreis():.2f} Euro"
korb = [
Artikel("Brot", 2.49),
Artikel("Mineralwasser", 0.49, pfand=0.25),
Artikel("Butter", 2.29),
]
for artikel in korb:
print(artikel)
print(f"Zusammen: {sum([artikel.kassenpreis() for artikel in korb]):.2f} Euro") Dasselbe Ergebnis geht auch ohne Vererbung. Jeder Artikel hat ein Pfand, und bei den meisten ist es
0. Eine Klasse statt zwei, keine Methode, die überschrieben wird, und nichts, wo ein super()
fehlen kann. Für einen Laden mit Pfand ist das ehrlich gesagt die bessere Lösung.
In Programmen dieser Größe braucht man Vererbung selten. Sie lohnt sich, wenn eine Unterklasse sich an mehreren Stellen wirklich anders verhält und nicht nur bei einem Wert, wenn sie also eine andere Art Ding ist und nicht bloß ein Ding mit einem Wert mehr. Eine Kette aus drei, vier Klassen, die nur da ist, weil es ging, macht Code schwerer statt leichter.
Häufiger begegnet dir Vererbung in fremdem Code, der sie verlangt. Eine eigene Fehlerart wie
class ZuWenigGeld(ValueError): pass ist so ein Fall. Sie erbt von ValueError, und deshalb fängt
ein except ValueError sie mit.
Was die Aufgabe will
Ein Sparkonto ist ein Konto mit Zinsen. Es kann alles, was ein Konto kann, und hat dazu einen Zinssatz und eine Methode, die einmal im Jahr die Zinsen gutschreibt.
Zum Mitnehmen
class Pfandartikel(Artikel): übernimmt alles von Artikel. Ein eigenes __init__ ruft als Erstes super().__init__(...) auf, sonst fehlen die Attribute der Elternklasse. Eine Methode mit gleichem Namen ersetzt die geerbte, über super() bleibt deren Ergebnis trotzdem erreichbar, und wenn sich eine Art nur in einem Wert unterscheidet, reicht ein Attribut mit Standardwert.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.