mitmario.dev

Vererbung

Python Sandbox 3 Min Lesezeit 3 BeispieleLektion 5 von 6

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

Ein Artikel mit Pfand
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()

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

Pfand als Attribut
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, kostenlos

Zu 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.