Vererbung in Python
Verstehe Vererbung in Python: Basisklassen, Unterklassen, super(), Mehrfachvererbung, abstrakte Klassen und das Prinzip Komposition vs. Vererbung.
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:
- Bevorzuge Komposition vor Vererbung - “Hat-ein” ist flexibler als “Ist-ein”
- Halte Vererbungshierarchien flach - Mehr als 3 Ebenen sind ein Warnzeichen
- Verwende Mixins sparsam - Sie erhoehen die Komplexitaet schnell
- Abstrakte Klassen sind gut - Sie erzwingen ein einheitliches Interface
- 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.