Zum Inhalt springen
Python Fortgeschritten 3 min

Vererbung in Python

Verstehe Vererbung in Python: Basisklassen, Unterklassen, super(), Mehrfachvererbung, abstrakte Klassen und das Prinzip Komposition vs. Vererbung.

Aktualisiert:

Vererbung in Python

Vererbung ist eines der zentralen Konzepte der objektorientierten Programmierung. Sie ermoeglicht es, bestehenden Code wiederzuverwenden und Hierarchien von Klassen aufzubauen.

Was ist Vererbung?

Vererbung bedeutet, dass eine neue Klasse (Unterklasse/Kindklasse) die Eigenschaften und Methoden einer bestehenden Klasse (Basisklasse/Elternklasse) uebernimmt - und sie erweitern oder anpassen kann.

Das spart Code und bildet logische Beziehungen ab:

Tier (Basisklasse)
├── Hund (Unterklasse) - erbt von Tier + kann bellen
├── Katze (Unterklasse) - erbt von Tier + kann schnurren
└── Vogel (Unterklasse) - erbt von Tier + kann fliegen

Ohne Vererbung muessten wir fuer jede Tierart den gleichen Code (Name, Alter, fressen, schlafen) neu schreiben. Mit Vererbung definieren wir das einmal in der Basisklasse.

Basis- und Unterklassen

# Basisklasse (auch: Elternklasse, Superklasse)
class Tier:
    def __init__(self, name, alter, art):
        self.name = name
        self.alter = alter
        self.art = art
        self.energie = 100

    def fressen(self, futter):
        self.energie = min(self.energie + 20, 100)
        print(f"{self.name} frisst {futter}. Energie: {self.energie}")

    def schlafen(self, stunden):
        self.energie = min(self.energie + stunden * 10, 100)
        print(f"{self.name} schlaeft {stunden} Stunden. Energie: {self.energie}")

    def __str__(self):
        return f"{self.name} ({self.art}, {self.alter} Jahre)"

# Unterklasse (auch: Kindklasse, Subklasse)
class Hund(Tier):  # Hund erbt von Tier
    def __init__(self, name, alter, rasse):
        super().__init__(name, alter, "Hund")  # Elternklasse initialisieren
        self.rasse = rasse
        self.tricks = []

    def bellen(self):
        self.energie -= 5
        print(f"{self.name} sagt: Wuff wuff!")

    def trick_lernen(self, trick):
        self.tricks.append(trick)
        print(f"{self.name} hat '{trick}' gelernt!")

    def tricks_zeigen(self):
        if self.tricks:
            print(f"{self.name} kann: {', '.join(self.tricks)}")
        else:
            print(f"{self.name} kennt noch keine Tricks.")

class Katze(Tier):
    def __init__(self, name, alter, farbe):
        super().__init__(name, alter, "Katze")
        self.farbe = farbe
        self.indoor = True

    def schnurren(self):
        print(f"{self.name} schnurrt zufrieden...")

    def jagen(self):
        self.energie -= 15
        print(f"{self.name} jagt eine Maus!")

# Verwendung
rex = Hund("Rex", 5, "Schaeferhund")
mieze = Katze("Mieze", 3, "schwarz")

# Geerbte Methoden funktionieren:
rex.fressen("Knochen")     # Rex frisst Knochen. Energie: 100
rex.schlafen(2)             # Rex schlaeft 2 Stunden. Energie: 100
print(rex)                  # Rex (Hund, 5 Jahre)

# Eigene Methoden der Unterklasse:
rex.bellen()                # Rex sagt: Wuff wuff!
rex.trick_lernen("Sitz")   # Rex hat 'Sitz' gelernt!

mieze.schnurren()           # Mieze schnurrt zufrieden...
mieze.jagen()               # Mieze jagt eine Maus!

Die super()-Funktion

super() gibt eine Referenz auf die Elternklasse zurueck. Damit rufst du Methoden der Elternklasse auf:

class Fahrzeug:
    def __init__(self, marke, baujahr):
        self.marke = marke
        self.baujahr = baujahr
        self.km_stand = 0

    def info(self):
        return f"{self.marke} (Baujahr {self.baujahr})"

class Elektroauto(Fahrzeug):
    def __init__(self, marke, baujahr, batterie_kwh):
        super().__init__(marke, baujahr)  # Eltern-__init__ aufrufen
        self.batterie_kwh = batterie_kwh
        self.ladestand = 100

    def info(self):
        # Eltern-Methode aufrufen und erweitern
        basis_info = super().info()
        return f"{basis_info} - Elektro, {self.batterie_kwh} kWh"

tesla = Elektroauto("Tesla Model 3", 2024, 75)
print(tesla.info())  # Tesla Model 3 (Baujahr 2024) - Elektro, 75 kWh

Warum super() und nicht direkt den Klassennamen?

# Funktioniert, aber NICHT empfohlen:
class Elektroauto(Fahrzeug):
    def __init__(self, marke, baujahr, batterie_kwh):
        Fahrzeug.__init__(self, marke, baujahr)  # Schlecht!

# Besser - mit super():
class Elektroauto(Fahrzeug):
    def __init__(self, marke, baujahr, batterie_kwh):
        super().__init__(marke, baujahr)  # Gut!

super() ist flexibler und funktioniert korrekt bei Mehrfachvererbung.

Methoden ueberschreiben

Eine Unterklasse kann Methoden der Elternklasse ueberschreiben (Override), um ein anderes Verhalten zu definieren:

class Form:
    def __init__(self, farbe):
        self.farbe = farbe

    def flaeche(self):
        return 0  # Standardwert

    def beschreibung(self):
        return f"Eine {self.farbe} Form mit Flaeche {self.flaeche():.2f}"

class Kreis(Form):
    def __init__(self, farbe, radius):
        super().__init__(farbe)
        self.radius = radius

    def flaeche(self):  # Ueberschreibt Form.flaeche()
        import math
        return math.pi * self.radius ** 2

class Rechteck(Form):
    def __init__(self, farbe, breite, hoehe):
        super().__init__(farbe)
        self.breite = breite
        self.hoehe = hoehe

    def flaeche(self):  # Ueberschreibt Form.flaeche()
        return self.breite * self.hoehe

class Dreieck(Form):
    def __init__(self, farbe, basis, hoehe):
        super().__init__(farbe)
        self.basis = basis
        self.hoehe = hoehe

    def flaeche(self):
        return 0.5 * self.basis * self.hoehe

# Polymorphismus: gleiche Methode, verschiedenes Verhalten
formen = [
    Kreis("rot", 5),
    Rechteck("blau", 4, 6),
    Dreieck("gruen", 3, 8)
]

for form in formen:
    print(form.beschreibung())
# Eine rot Form mit Flaeche 78.54
# Eine blau Form mit Flaeche 24.00
# Eine gruen Form mit Flaeche 12.00

isinstance() und issubclass()

Mit diesen Funktionen pruefst du Vererbungsbeziehungen:

rex = Hund("Rex", 5, "Schaeferhund")
mieze = Katze("Mieze", 3, "schwarz")

# isinstance() - Ist ein Objekt eine Instanz einer Klasse?
print(isinstance(rex, Hund))    # True
print(isinstance(rex, Tier))    # True  (Hund IST ein Tier)
print(isinstance(rex, Katze))   # False
print(isinstance(mieze, Tier))  # True

# issubclass() - Ist eine Klasse eine Unterklasse?
print(issubclass(Hund, Tier))    # True
print(issubclass(Katze, Tier))   # True
print(issubclass(Hund, Katze))   # False
print(issubclass(Tier, object))  # True  (alles erbt von object)

In Python erbt jede Klasse automatisch von object - auch wenn du es nicht hinschreibst.

Mehrfachvererbung und MRO

Python erlaubt es, von mehreren Klassen gleichzeitig zu erben:

class Schwimmbar:
    def schwimmen(self):
        print(f"{self.name} schwimmt!")

class Fliegbar:
    def fliegen(self):
        print(f"{self.name} fliegt!")

class Laufbar:
    def laufen(self):
        print(f"{self.name} laeuft!")

class Ente(Tier, Schwimmbar, Fliegbar, Laufbar):
    def __init__(self, name, alter):
        super().__init__(name, alter, "Ente")

    def quaken(self):
        print(f"{self.name} sagt: Quak quak!")

donald = Ente("Donald", 2)
donald.schwimmen()  # Donald schwimmt!
donald.fliegen()    # Donald fliegt!
donald.laufen()     # Donald laeuft!
donald.quaken()     # Donald sagt: Quak quak!
donald.fressen("Brot")  # Geerbt von Tier

Method Resolution Order (MRO)

Wenn mehrere Elternklassen die gleiche Methode haben, bestimmt die MRO, welche zuerst aufgerufen wird:

class A:
    def wer_bin_ich(self):
        print("Ich bin A")

class B(A):
    def wer_bin_ich(self):
        print("Ich bin B")

class C(A):
    def wer_bin_ich(self):
        print("Ich bin C")

class D(B, C):
    pass

d = D()
d.wer_bin_ich()  # Ich bin B

# MRO anzeigen:
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)

Python verwendet den C3-Linearisierungsalgorithmus fuer die MRO. Die Reihenfolge ist: D -> B -> C -> A -> object (von links nach rechts, Tiefe zuletzt).

Das Diamond-Problem

Das Diamond-Problem entsteht, wenn eine Klasse ueber verschiedene Wege von derselben Basisklasse erbt:

      A
     / \
    B   C
     \ /
      D
class A:
    def __init__(self):
        print("A.__init__")
        self.wert = "A"

class B(A):
    def __init__(self):
        print("B.__init__")
        super().__init__()
        self.wert = "B"

class C(A):
    def __init__(self):
        print("C.__init__")
        super().__init__()
        self.wert = "C"

class D(B, C):
    def __init__(self):
        print("D.__init__")
        super().__init__()

d = D()
# Ausgabe (dank MRO und super()):
# D.__init__
# B.__init__
# C.__init__
# A.__init__

print(d.wert)  # B (B ueberschreibt zuletzt)

Wichtig: Dank super() und MRO wird A.__init__ nur einmal aufgerufen, nicht zweimal. Ohne super() koennte A doppelt initialisiert werden.

Abstrakte Klassen

Manchmal willst du eine Basisklasse definieren, von der man keine Instanzen erstellen darf - sie dient nur als Vorlage. Dafuer gibt es das abc-Modul:

from abc import ABC, abstractmethod

class Zahlungsmethode(ABC):
    """Abstrakte Basisklasse - kann nicht instanziiert werden."""

    def __init__(self, inhaber):
        self.inhaber = inhaber

    @abstractmethod
    def bezahlen(self, betrag):
        """Muss von jeder Unterklasse implementiert werden."""
        pass

    @abstractmethod
    def saldo_pruefen(self):
        """Muss von jeder Unterklasse implementiert werden."""
        pass

    def beleg_drucken(self, betrag):
        """Konkrete Methode - wird geerbt."""
        print(f"Beleg: {betrag:.2f} EUR bezahlt von {self.inhaber}")

# Das geht NICHT:
# z = Zahlungsmethode("Test")  # TypeError!

class Kreditkarte(Zahlungsmethode):
    def __init__(self, inhaber, kartennummer, limit):
        super().__init__(inhaber)
        self.kartennummer = kartennummer
        self.limit = limit
        self.ausgaben = 0

    def bezahlen(self, betrag):
        if self.ausgaben + betrag > self.limit:
            print("Limit ueberschritten!")
            return False
        self.ausgaben += betrag
        self.beleg_drucken(betrag)
        return True

    def saldo_pruefen(self):
        verfuegbar = self.limit - self.ausgaben
        print(f"Verfuegbar: {verfuegbar:.2f} EUR von {self.limit:.2f} EUR")
        return verfuegbar

class PayPal(Zahlungsmethode):
    def __init__(self, inhaber, email, guthaben):
        super().__init__(inhaber)
        self.email = email
        self.guthaben = guthaben

    def bezahlen(self, betrag):
        if betrag > self.guthaben:
            print("Nicht genug Guthaben!")
            return False
        self.guthaben -= betrag
        self.beleg_drucken(betrag)
        return True

    def saldo_pruefen(self):
        print(f"PayPal-Guthaben: {self.guthaben:.2f} EUR")
        return self.guthaben

# Verwendung
karte = Kreditkarte("Anna", "1234-5678", 2000)
paypal = PayPal("Bob", "bob@email.de", 150)

karte.bezahlen(49.99)      # Beleg: 49.99 EUR bezahlt von Anna
karte.saldo_pruefen()       # Verfuegbar: 1950.01 EUR von 2000.00 EUR
paypal.bezahlen(29.99)      # Beleg: 29.99 EUR bezahlt von Bob

Komposition vs. Vererbung

Nicht jede Beziehung ist eine “ist-ein”-Beziehung. Manchmal ist “hat-ein” besser:

# FALSCH: Vererbung fuer "hat-ein"-Beziehungen
class Motor:
    def starten(self):
        print("Motor startet")

# Schlecht! Ein Auto IST kein Motor
class Auto(Motor):
    pass

# RICHTIG: Komposition fuer "hat-ein"-Beziehungen
class Motor:
    def __init__(self, ps):
        self.ps = ps
        self.laeuft = False

    def starten(self):
        self.laeuft = True
        print(f"Motor ({self.ps} PS) gestartet")

    def stoppen(self):
        self.laeuft = False
        print("Motor gestoppt")

class Klimaanlage:
    def __init__(self):
        self.temperatur = 22
        self.aktiv = False

    def einschalten(self, temp):
        self.aktiv = True
        self.temperatur = temp
        print(f"Klimaanlage auf {temp}°C eingestellt")

class Auto:
    """Auto HAT einen Motor und EINE Klimaanlage."""

    def __init__(self, marke, ps):
        self.marke = marke
        self.motor = Motor(ps)          # Komposition
        self.klima = Klimaanlage()       # Komposition

    def starten(self):
        print(f"{self.marke} wird gestartet...")
        self.motor.starten()

    def kuehl_fahren(self, temperatur):
        self.motor.starten()
        self.klima.einschalten(temperatur)
        print(f"{self.marke} faehrt gekuehlt los!")

bmw = Auto("BMW", 200)
bmw.kuehl_fahren(19)
# BMW wird gestartet...
# Motor (200 PS) gestartet
# Klimaanlage auf 19°C eingestellt
# BMW faehrt gekuehlt los!

Faustregel:

  • Vererbung (ist-ein): Ein Hund ist ein Tier
  • Komposition (hat-ein): Ein Auto hat einen Motor

Praxis: Tier-Hierarchie

from abc import ABC, abstractmethod

class Tier(ABC):
    def __init__(self, name, alter, gewicht):
        self.name = name
        self.alter = alter
        self.gewicht = gewicht

    @abstractmethod
    def laut_geben(self):
        pass

    def __str__(self):
        return f"{self.name} ({self.__class__.__name__}, {self.alter} Jahre)"

class Haustier:
    """Mixin-Klasse fuer Haustiere."""
    def __init__(self, besitzer=None):
        self.besitzer = besitzer
        self.geimpft = False

    def impfen(self):
        self.geimpft = True
        print(f"{self.name} wurde geimpft.")

class Hund(Tier, Haustier):
    def __init__(self, name, alter, gewicht, rasse, besitzer=None):
        Tier.__init__(self, name, alter, gewicht)
        Haustier.__init__(self, besitzer)
        self.rasse = rasse

    def laut_geben(self):
        return "Wuff!"

class Katze(Tier, Haustier):
    def __init__(self, name, alter, gewicht, farbe, besitzer=None):
        Tier.__init__(self, name, alter, gewicht)
        Haustier.__init__(self, besitzer)
        self.farbe = farbe

    def laut_geben(self):
        return "Miau!"

class Wolf(Tier):
    def __init__(self, name, alter, gewicht, rudel):
        super().__init__(name, alter, gewicht)
        self.rudel = rudel

    def laut_geben(self):
        return "Auuuu!"

# Verwendung
tiere = [
    Hund("Bello", 4, 25, "Labrador", "Max"),
    Katze("Luna", 2, 4, "grau", "Lisa"),
    Wolf("Graubart", 7, 45, "Nordrudel")
]

for tier in tiere:
    print(f"{tier} sagt: {tier.laut_geben()}")

# Nur Haustiere impfen
for tier in tiere:
    if isinstance(tier, Haustier):
        tier.impfen()

Praxis: Fahrzeug-System

from abc import ABC, abstractmethod

class Fahrzeug(ABC):
    def __init__(self, marke, modell, baujahr, max_geschwindigkeit):
        self.marke = marke
        self.modell = modell
        self.baujahr = baujahr
        self.max_geschwindigkeit = max_geschwindigkeit
        self.km_stand = 0

    @abstractmethod
    def tanken(self, menge):
        pass

    @abstractmethod
    def reichweite(self):
        pass

    def fahren(self, km):
        self.km_stand += km
        print(f"{self.marke} {self.modell}: {km} km gefahren. "
              f"Gesamt: {self.km_stand} km")

    def __str__(self):
        return f"{self.marke} {self.modell} ({self.baujahr})"

class Verbrenner(Fahrzeug):
    def __init__(self, marke, modell, baujahr, max_geschwindigkeit,
                 tankgroesse, verbrauch):
        super().__init__(marke, modell, baujahr, max_geschwindigkeit)
        self.tankgroesse = tankgroesse    # Liter
        self.verbrauch = verbrauch        # Liter/100km
        self.tankfuellung = tankgroesse   # Startet voll

    def tanken(self, liter):
        self.tankfuellung = min(self.tankfuellung + liter, self.tankgroesse)
        print(f"Getankt: {self.tankfuellung:.1f}/{self.tankgroesse} Liter")

    def reichweite(self):
        return (self.tankfuellung / self.verbrauch) * 100

class Elektroauto(Fahrzeug):
    def __init__(self, marke, modell, baujahr, max_geschwindigkeit,
                 batterie_kwh, verbrauch_kwh):
        super().__init__(marke, modell, baujahr, max_geschwindigkeit)
        self.batterie_kwh = batterie_kwh
        self.verbrauch_kwh = verbrauch_kwh  # kWh/100km
        self.ladestand = batterie_kwh       # Startet voll

    def tanken(self, kwh):
        self.ladestand = min(self.ladestand + kwh, self.batterie_kwh)
        prozent = (self.ladestand / self.batterie_kwh) * 100
        print(f"Geladen: {prozent:.0f}% ({self.ladestand:.1f}/{self.batterie_kwh} kWh)")

    def reichweite(self):
        return (self.ladestand / self.verbrauch_kwh) * 100

class Hybrid(Verbrenner, Elektroauto):
    def __init__(self, marke, modell, baujahr, max_geschwindigkeit,
                 tankgroesse, verbrauch, batterie_kwh, verbrauch_kwh):
        Fahrzeug.__init__(self, marke, modell, baujahr, max_geschwindigkeit)
        self.tankgroesse = tankgroesse
        self.verbrauch = verbrauch
        self.tankfuellung = tankgroesse
        self.batterie_kwh = batterie_kwh
        self.verbrauch_kwh = verbrauch_kwh
        self.ladestand = batterie_kwh

    def tanken(self, menge):
        print("Hybrid: Tanke Benzin und lade Batterie...")
        self.tankfuellung = min(self.tankfuellung + menge, self.tankgroesse)
        self.ladestand = self.batterie_kwh
        print(f"Tank: {self.tankfuellung:.1f}L, Batterie: 100%")

    def reichweite(self):
        benzin_km = (self.tankfuellung / self.verbrauch) * 100
        elektro_km = (self.ladestand / self.verbrauch_kwh) * 100
        return benzin_km + elektro_km

# Flotte erstellen
flotte = [
    Verbrenner("VW", "Golf", 2023, 220, 50, 6.5),
    Elektroauto("Tesla", "Model 3", 2024, 225, 75, 15),
    Hybrid("Toyota", "Prius", 2024, 180, 43, 4.5, 8.8, 10)
]

print("=== Flottenübersicht ===")
for fahrzeug in flotte:
    print(f"{fahrzeug} - Reichweite: {fahrzeug.reichweite():.0f} km")

Uebungen

Uebung 1: Mitarbeiter-Hierarchie

Erstelle eine Basisklasse Mitarbeiter mit Name und Gehalt. Leite davon Manager (mit Bonus und Team-Liste), Entwickler (mit Programmiersprache) und Praktikant (mit Universitaet) ab. Jede Klasse soll gehalt_berechnen() ueberschreiben.

Uebung 2: Abstrakte Medien-Klasse

Erstelle eine abstrakte Klasse Medium mit den Methoden abspielen(), pausieren() und info(). Implementiere Unterklassen Song, Video und Podcast.

Uebung 3: Formen-Rechner

Erstelle eine abstrakte Klasse Form mit flaeche() und umfang(). Implementiere Kreis, Rechteck, Dreieck und Quadrat (erbt von Rechteck). Erstelle eine Funktion, die die groesste Form aus einer Liste findet.

Uebung 4: Mixins

Erstelle Mixin-Klassen Serialisierbar (mit to_json() und from_json()), Druckbar (mit drucken()) und Vergleichbar (mit __eq__ und __lt__). Kombiniere sie mit einer Produkt-Klasse.

Pro-Tipp: Vererbung richtig einsetzen

Vererbung ist maechtig, aber wird oft ueberstrapaziert. Beachte diese Regeln:

  1. Bevorzuge Komposition vor Vererbung - “Hat-ein” ist flexibler als “Ist-ein”
  2. Halte Vererbungshierarchien flach - Mehr als 3 Ebenen sind ein Warnzeichen
  3. Verwende Mixins sparsam - Sie erhoehen die Komplexitaet schnell
  4. Abstrakte Klassen sind gut - Sie erzwingen ein einheitliches Interface
  5. Vermeide Mehrfachvererbung wenn moeglich - Das Diamond-Problem ist real

Eine bewaehrte Faustregel: Wenn du nicht sicher bist, ob Vererbung oder Komposition besser passt, waehle Komposition. Du kannst spaeter immer noch umbauen.

Zurück zum Python Kurs