21.2 Vererbung 

Neben der strukturellen Verschmelzung von Daten und den darauf arbeitenden Methoden zu einer Einheit zielt das Konzept der Objektorientierung darauf ab, die Wiederverwendbarkeit von Programm-Code zu verbessern. Damit ist gemeint, dass ein Programm mit geringem Aufwand an Probleme angepasst werden kann, die dem Problem ähnlich sind, für das das Programm ursprünglich entwickelt wurde.
Konkret bedeutet dies, dass man von bereits bestehenden Klassen neue Klassen ableitet, um diese um zusätzliche Funktionalität zu erweitern. Dabei übernimmt die abgeleitete Klasse alle Fähigkeiten von ihrer Basisklasse, sodass sie zunächst eine Kopie dieser Klasse ist. Man sagt, die Basisklasse vererbt ihre Fähigkeiten an eine Tochterklasse. Nach diesem Vererbungsschritt kann man die abgeleitete Klasse an die neuen Anforderungen anpassen.
Bevor wir Vererbung auf unser konkretes Beispiel anwenden, werden wir Ihnen an einigen abstrakten Beispielen zeigen, wie Python dieses Konzept technisch umsetzt.
21.2.1 Technische Grundlagen 

Um eine Klasse von einer anderen erben zu lassen, schreibt man bei der Definition der Tochterklasse die Basisklasse in Klammern hinter den Klassennamen. Im folgenden Beispiel erbt also die Klasse B von der Klasse A:
class A:
pass
class B(A):
pass
Diese Klassen A und B sind noch sehr langweilig, da sie keine Methoden oder Attribute besitzen. Daher erweitern wir unsere Klassen folgendermaßen:
class A:
def __init__(self):
self.X = 1337
print("Konstruktor von A")
def m(self):
print("Methode m von A. Es ist self.X =", self.X)
class B(A):
def n(self):
print("Methode n von B")
b = B()
b.n()
b.m()
In diesem Beispiel wird die Klasse A um einen Konstruktor erweitert, der ein Attribut X mit dem Wert 1337 erzeugt. Zusätzlich erhält die Klasse A eine Methode m. Sowohl der Konstruktor als auch die Methode m geben jeweils eine Meldung auf dem Bildschirm aus. Außerdem versehen wir die Klasse B mit einer Methode n, die ebenfalls eine Meldung ausgibt. Am Ende des kleinen Programms werden eine Instanz der Klasse B erzeugt und ihre Methoden n und m gerufen.
Die Ausgabe zeigt, dass B sowohl den Konstruktor als auch die Methode m von der Klasse A geerbt hat. Auch das Attribut X wurde ordnungsgemäß angelegt.
Konstruktor von A
Methode n von B
Methode m von A. Es ist self.X = 1337
Der Konstruktor einer Klasse hat die Aufgabe, die Klasse in einen wohldefinierten Initialzustand zu bringen. Wie die Ausgabe des oben dargestellten Programms zeigt, wurde beim Erzeugen einer Instanz der Klasse B der Konstruktor der Klasse A gerufen. Nun ist es in der Praxis häufig so, dass eine abgeleitete Klasse einen anderen Konstruktor als ihre Basisklasse benötigt, um eigene Initialisierungen vorzunehmen.
Überschreiben von Methoden
Wir erweitern daher unsere Klasse B um einen eigenen Konstruktor, der ein Attribut Y anlegt und auch eine Ausgabe erzeugt. Zusätzlich erweitern wir die Methode n so, dass sie den Wert des Attributs Y ausgibt.
class B(A):
def __init__(self):
self.Y = 10000
print("Konstruktor von B")
def n(self):
print("Methode n von B. Es ist self.Y =", self.Y)
b = B()
b.n()
b.m()
Die Ausgabe dieses Beispiels überrascht uns mit einer Fehlermeldung:
Konstruktor von B
Methode n von B. Es ist self.Y = 10000
Traceback (most recent call last):
…
AttributeError: 'B' object has no attribute 'X'
Laut der Bildschirmausgabe werden der Konstruktor von B sowie die Methoden n und m gerufen. Allerdings beschwert sich die Methode m darüber, dass die Instanz kein Attribut X besitzt.
Dies ist nicht verwunderlich, da der Konstruktor von A, der für das Anlegen des Attributs X zuständig ist, nicht aufgerufen wird. Dieses Verhalten ist folgendermaßen begründet:
Die Klasse B hat die Methode __init__ – also den Konstruktor – zunächst von der Klasse A geerbt, sie aber dann mit ihrem eigenen Konstruktor überschrieben. Infolgedessen wird beim Erzeugen einer Instanz der Klasse B nur noch der neue von B definierte Konstruktor gerufen, während der Konstruktor von A nicht zum Zuge kommt.
Generell spricht man vom Überschreiben einer Methode, wenn eine Klasse eine Methode erneut implementiert, die sie bereits von ihrer Basisklasse geerbt hat.
Im Allgemeinen ist es aber erforderlich, dass der überschriebene Konstruktor der Basisklasse gerufen wird, um die Instanz in einen konsistenten Zustand zu versetzen. Daher ist es möglich, überschriebene Methoden der Basisklasse explizit zu rufen:
class B(A):
def __init__(self):
super().__init__()
self.Y = 10000
print("Konstruktor von B")
def n(self):
print("Methode n von B. Es ist self.Y =", self.Y)
b = B()
b.n()
b.m()
Mit der Zeile super().__init__() rufen wir im Konstruktor der Klasse B explizit den Konstruktor der Basisklasse A. Die Built-in Function super findet dabei für uns heraus, dass A die Basisklasse von B ist und dass deshalb mit super().__init__() die __init__-Methode von A gerufen werden soll.
Die Ausgabe des oben dargestellten Codes zeigt, dass der Konstruktor von A nun wie gewünscht aufgerufen wird, und auch der Aufruf der Methode m funktioniert wieder.
Konstruktor von A
Konstruktor von B
Methode n von B. Es ist self.Y = 10000
Methode m von A. Es ist self.X = 1337
Dieses Überschreiben von Methoden ist nicht auf den Konstruktor beschränkt, und es kann auch jede beliebige Methode der Basisklasse wie der Konstruktor im obigen Beispiel explizit gerufen werden. Zur Illustration überschreiben wir im folgenden Beispiel in der Klasse B die Methode m von A und nutzen super, um wieder m von A aufzurufen:
class B(A):
def __init__(self):
super().__init__()
self.Y = 10000
print("Konstruktor von B")
def n(self):
print("Methode n von B. Es ist self.Y =", self.Y)
def m(self):
print("Methode m von B.")
super().m()
b = B()
b.m()
Die Ausgabe dieses Beispielprogramms lautet:
Konstruktor von A
Konstruktor von B
Methode m von B.
Methode m von A. Es ist self.X = 1337
Durch super().m() wurde also wie gewünscht die Methode m der Basisklasse A gerufen.
[»] Hinweis
Methoden der Basisklasse lassen sich auch ohne super explizit aufrufen. Im Konstruktor von B kann super().__init__() durch A.__init__(self) ersetzt werden, um den Konstruktor von A zu rufen. Allerdings muss dazu bei jedem Aufruf die Basisklasse explizit angegeben werden, obwohl sie aus dem Kontext klar ist.
Nun haben wir das Werkzeug an der Hand, um das Konzept Vererbung auf unser Kontobeispiel anzuwenden. Dabei werden wir unser Programm in mehrere Klassen zerlegen, die voneinander erben.
21.2.2 Die Klasse GirokontoMitTagesumsatz 

Objektorientierte Programmierung zielt darauf ab, Vorhandenes erneut zu verwenden bzw. Code bereitzustellen, der einfach an neue Anforderungen angepasst werden kann. Dies hat zur Folge, dass Sie bei der Entwicklung eines objektorientierten Programms immer darauf achten sollten, Ihre Klassen möglichst universell zu halten. Erst dadurch wird es möglich, Teile des Programms durch geschickte Vererbung für die Lösung neuer Probleme zu übernehmen.
Wir werden als Beispiel eine Klasse GirokontoMitTagesumsatz entwickeln, die das Gleiche leistet wie die oben präsentierte Klasse Konto. Allerdings werden wir diesmal darauf achten, unseren Programm-Code so zu strukturieren, dass er leicht für ähnliche Aufgaben verwendet werden kann.
Ausgangspunkt unseres Programms ist die Klasse Konto aus Abschnitt 21.1, deren Attribute sich zunächst in zwei Kategorien einteilen lassen:
- Daten, die den Umgang mit dem Geld auf dem Konto betreffen (Kontostand, MaxTagesumsatz, UmsatzHeute)
- Daten, die den Kunden betreffen (Inhaber, Kontonummer)
Alle Methoden mit Ausnahme der Methode zeige verwenden nur Attribute der ersten Kategorie. Daher nehmen wir an dieser Stelle die erste strukturelle Trennung vor, indem wir ein Konto in zwei Teile aufspalten.
Der eine Teil soll sich um die Verwaltung des Kontostands kümmern, und der andere Teil soll die Kundendaten speichern.
Die Klasse VerwalteterGeldbetrag
Abstrakt gesehen muss eine Klasse, die den Kontostand unseres Kontos verwaltet, Einzahlungen, Auszahlungen und Geldtransfers zu anderen Konten unterstützen. Diese Operationen müssen an bestimmte Bedingungen gekoppelt werden können, nämlich, ob die jeweiligen maximalen Tagesumsätze eingehalten werden oder nicht.
Neben Konten gibt es aber weitere Gebilde, die einen Geldbetrag nach bestimmten Regeln verwalten. Beispielsweise lässt sich das Geld, das sich in einer Geldbörse befindet, als Kontostand interpretieren. Die Operationen Einzahlen und Auszahlen beschreiben dann den Vorgang, Bargeld in die Geldbörse zu geben bzw. Bargeld aus dieser zu entnehmen. Ähnlich verhält es sich bei einem Tresor oder dem Guthaben auf einer Prepaid-Karte.
Es ist daher sinnvoll, eine Klasse zu implementieren, die es ermöglicht, einen Geldbetrag nach bestimmten Regeln zu verwalten. Diese Klasse VerwalteterGeldbetrag wird dann als Basis für unsere Klasse GirokontoMitTagesumsatz dienen, bleibt aber weiterhin nützlich für andere Anwendungen.
class VerwalteterGeldbetrag:
def __init__(self, anfangsbetrag):
self.Betrag = anfangsbetrag
def einzahlenMoeglich(self, betrag):
return True
def auszahlenMoeglich(self, betrag):
return True
def einzahlen(self, betrag):
if betrag < 0 or not self.einzahlenMoeglich(betrag):
return False
else:
self.Betrag += betrag
return True
def auszahlen(self, betrag):
if betrag < 0 or not self.auszahlenMoeglich(betrag):
return False
else:
self.Betrag -= betrag
return True
def zeige(self):
print("Betrag: {:.2f}".format(self.Betrag))
Im Konstruktor der Klasse wird das Attribut Betrag angelegt und auf den übergebenen Initialwert gesetzt. Über die Methoden einzahlen und auszahlen kann der Betrag verändert werden, wobei jeweils True zurückgegeben wird, wenn die Operation erfolgreich war, und False, falls ein Problem aufgetreten ist. Die Methode zeige gibt den aktuell vorhandenen Betrag auf dem Bildschirm aus.
Der Clou der Klasse VerwalteterGeldbetrag liegt in den Methoden einzahlenMoeglich und auszahlenMoeglich, mit denen die Methoden einzahlen bzw. auszahlen prüfen, ob die jeweilige Operation ausgeführt werden kann.
Sie sind dazu gedacht, von abgeleiteten Klassen überschrieben zu werden, um die gewünschten Bedingungen festzulegen. Da sie in der Klasse VerwalteterGeldbetrag den Wert True zurückgeben, sind Einzahlungen und Auszahlungen ohne Einschränkungen möglich, solange diese Methoden nicht überschrieben werden.
Die Klasse AllgemeinesKonto
Unserer Klasse VerwalteterGeldbetrag fehlt unter anderem noch die Möglichkeit, Geld zwischen verschiedenen Instanzen zu transferieren, um die Funktionalität unserer Ausgangsklasse Konto nachzubilden. Da dies ein Vorgang ist, der von sämtlichen Konten beherrscht werden soll, werden wir nun eine Klasse AllgemeinesKonto von VerwalteterGeldbetrag ableiten und sie um eine Methode geldtransfer erweitern.
Außerdem gehören zu einem Konto immer die Kundendaten des jeweiligen Kontoinhabers. Diese werden wir in dem Attribut Kundendaten ablegen, dessen Wert den ersten Parameter des Konstruktors festlegt. Um die Definition der Klasse, mit der die Kundendaten gespeichert werden, kümmern wir uns später.
class AllgemeinesKonto(VerwalteterGeldbetrag):
def __init__(self, kundendaten, kontostand):
super().__init__(kontostand)
self.Kundendaten = kundendaten
def geldtransfer(self, ziel, betrag):
if self.auszahlenMoeglich(betrag) and ziel.einzahlenMoeglich(betrag):
self.auszahlen(betrag)
ziel.einzahlen(betrag)
return True
else:
return False
def zeige(self):
self.Kundendaten.zeige()
VerwalteterGeldbetrag.zeige(self)
Die neue Methode geldtransfer greift auf die Methoden auszahlenMoeglich und einzahlenMoeglich zurück, um die Machbarkeit des Transfers zu prüfen. Für den Transfer selbst werden die Methoden auszahlen und einzahlen verwendet.
Um eine Instanz der Klasse AllgemeinesKonto auszugeben, wird die Methode zeige überschrieben, sodass zunächst die Kundendaten ausgegeben werden und anschließend die Methode zeige der Basisklasse VerwalteterGeldbetrag gerufen wird. Dabei wird vorausgesetzt, dass die Instanz, die vom Attribut Kundendaten referenziert wird, eine Methode namens zeige besitzt.
Die Klasse AllgemeinesKontoMitTagesumsatz
Nun ist es an der Zeit, die Klasse AllgemeinesKonto um die Fähigkeit zu erweitern, den Tagesumsatz zu begrenzen. Zu diesem Zweck leiten wir die Klasse AllgemeinesKontoMitTagesumsatz von AllgemeinesKonto ab und überschreiben einige der Methoden.
class AllgemeinesKontoMitTagesumsatz(AllgemeinesKonto):
def __init__(self, kundendaten, kontostand, max_tagesumsatz=1500):
super().__init__(kundendaten, kontostand)
self.MaxTagesumsatz = max_tagesumsatz
self.UmsatzHeute = 0.0
def transferMoeglich(self, betrag):
return (self.UmsatzHeute + betrag <= self.MaxTagesumsatz)
def auszahlenMoeglich(self, betrag):
return self.transferMoeglich(betrag)
def einzahlenMoeglich(self, betrag):
return self.transferMoeglich(betrag)
def einzahlen(self, betrag):
if AllgemeinesKonto.einzahlen(self, betrag):
self.UmsatzHeute += betrag
return True
else:
return False
def auszahlen(self, betrag):
if AllgemeinesKonto.auszahlen(self, betrag):
self.UmsatzHeute += betrag
return True
else:
return False
def zeige(self):
AllgemeinesKonto.zeige(self)
print("Heute schon {:.2f} von {:.2f} Euro umgesetzt".format(
self.UmsatzHeute, self.MaxTagesumsatz))
Es werden die Methoden einzahlenMoeglich und auszahlenMoeglich überschrieben, sodass sie – abhängig vom Tagesumsatz – Einzahlungen und Auszahlungen ermöglichen oder blockieren. Beide Methoden greifen dafür auf die neue Methode transferMoeglich zurück.
Die Methoden einzahlen und auszahlen werden so angepasst, dass sie das Attribut UmsatzHeute gegebenenfalls aktualisieren. Zu guter Letzt fügt die zeige-Methode der Ausgabe von AllgemeinesKonto.zeige Informationen über den Tagesumsatz hinzu.
Damit verfügt die Klasse AllgemeinesKontoMitTagesumsatz über die gleiche Funktionalität, den Kontostand zu verwalten, wie unsere Ausgangsklasse Konto. Was noch fehlt, ist die Verwaltung der Kundendaten.
Die Klasse GirokontoDaten
Die mit einem Girokonto assoziierten Kundendaten werden in Instanzen der Klasse GirokontoKundendaten abgelegt. Neben zwei Attributen, die den Namen des Kontoinhabers sowie die Kontonummer speichern, verfügt auch diese Klasse über eine Methode zeige, um die Informationen auf dem Bildschirm auszugeben.
class GirokontoKundendaten:
def __init__(self, inhaber, kontonummer):
self.Inhaber = inhaber
self.Kontonummer = kontonummer
def zeige(self):
print("Inhaber:", self.Inhaber)
print("Kontonummer:", self.Kontonummer)
Nun können wir die Klasse GirokontoMitTagesumsatz definieren.
Die Klasse GirokontoMitTagesumsatz
Abschließend leiten wir die Klasse GirokontoMitTagesumsatz von der Klasse AllgemeinesKontoMitTagesumsatz ab und versehen sie durch Überschreiben des Konstruktors mit passenden Kundendaten.
class GirokontoMitTagesumsatz(AllgemeinesKontoMitTagesumsatz):
def __init__(self, inhaber, kontonummer, kontostand,
max_tagesumsatz=1500):
kundendaten = GirokontoKundendaten(inhaber, kontonummer)
super().__init__(kundendaten, kontostand, max_tagesumsatz)
Diese Klasse bildet den gesamten Funktionsumfang der Klasse Konto ab, sodass wir unser Eingangsbeispiel von Herrn Meier und Herrn Schmidt ausführen können.
>>> k1 = GirokontoMitTagesumsatz("Heinz Meier", 567123, 12350.0)
>>> k2 = GirokontoMitTagesumsatz("Erwin Schmidt", 396754, 15000.0)
>>> k1.geldtransfer(k2, 160)
True
>>> k2.geldtransfer(k1, 1000)
True
>>> k2.geldtransfer(k1, 500)
False
>>> k2.einzahlen(500)
False
>>> k1.zeige()
Inhaber: Heinz Meier
Kontonummer: 567123
Betrag: 13190.00
Heute schon 1160.00 von 1500.00 Euro umgesetzt
>>> k2.zeige()
Inhaber: Erwin Schmidt
Kontonummer: 396754
Betrag: 14160.00
Heute schon 1160.00 von 1500.00 Euro umgesetzt
Im nächsten Abschnitt werden wir verdeutlichen, dass unser Programm durch diese Strukturierung leicht zu erweitern ist.
21.2.3 Mögliche Erweiterungen der Klasse Konto 

Wir haben nun durch stückweise Verfeinerung mittels Vererbung aus unserer anfänglichen Idee des verwalteten Geldbetrags die Klasse GirokontoMitTagesumsatz abgeleitet. Diese Klasse verfügt nun über den gleichen Funktionsumfang wie die Klasse Konto. Abbildung 21.2 veranschaulicht die entstehende Klassenhierarchie grafisch.
Der Nutzen dieser Strukturierung wird deutlich, wenn wir neue Klassen einführen, die auf bereits vorhandene Funktionalität zurückgreifen können. Als Beispiel dienen dazu die Klassen Geldboerse, Tresor, Girokonto, Nummernkonto und NummernkontoMitTagesumsatz.
Bevor wir die Beschreibung und Implementation dieser Klassen besprechen, werfen wir einen Blick auf die neu entstehende Klassenhierarchie, wie sie Abbildung 21.3 zeigt.
Abbildung 21.2 Die Klassenhierarchie des Kontobeispiels
Abbildung 21.3 Eine erweiterte Klassenhierarchie des Kontobeispiels
Die Klassen Geldboerse und Tresor verwalten jeweils einen Bargeldbetrag, weshalb wir von der Klasse VerwalteterGeldbetrag zunächst eine Klasse VerwalteterBargeldbetrag ableiten. Im Unterschied zum allgemeinen verwalteten Geldbetrag kann ein Bargeldbetrag nicht negativ sein. Daher überschreibt die Klasse VerwalteterBargeldbetrag die Methode auszahlenMoeglich, um negative Beträge zu verhindern.
Zusätzlich zu dem Girokonto, dessen Transaktionen durch einen maximalen Tagesumsatz limitiert sind, modellieren wir nun Girokonten ohne Limitierung der Umsätze durch die Klasse Girokonto. Diese Klasse wird direkt von der Klasse AllgemeinesKonto abgeleitet und verwendet dieselben Kundendaten wie GirokontoMitTagesumsatz.
Um neben Girokonten auch Nummernkonten verwalten zu können, legen wir eine neue Klasse an, mit der die Kundendaten eines Nummernkontos verwaltet werden können.[ 90 ](Diese Klasse NummernkontoKundendaten ist genau wie die Klasse GirokontoKundendaten nicht in Abbildung 21.3 aufgeführt, da sie nicht von VerwalteterGeldbetrag erben. ) Damit lassen sich dann die Klassen Nummernkonto und NummernkontoMitTagesumsatz von den Klassen AllgemeinesKonto bzw. AllgemeinesKontoMitTagesumsatz ableiten.
Nun schauen wir uns an, wie die Klassen Geldboerse, Tresor, Girokonto, Nummernkonto und NummernkontoMitTagesumsatz implementiert werden können.
Die Klassen VerwalteterBargeldbetrag, Geldboerse und Tresor
Die Klasse VerwalteterBargeldbetrag passt die Klasse VerwalteterGeldbetrag so an, dass sie einen negativen Wert für das Attribut Betrag verhindert.
class VerwalteterBargeldbetrag(VerwalteterGeldbetrag):
def __init__(self, bargeldbetrag):
if bargeldbetrag < 0:
bargeldbetrag = 0
super().__init__(bargeldbetrag)
def auszahlenMoeglich(self, betrag):
return (self.Betrag >= betrag)
Im Konstruktor wird dafür gesorgt, dass der Betrag nicht mit einem negativen Wert initialisiert werden kann, und die Methode auszahlenMoeglich liefert genau dann True zurück, wenn der Betrag in der Geldbörse mindestens so groß ist wie der Betrag, der ausgezahlt werden soll.
Die Klassen Geldboerse und Tresor erben nun von der Klasse VerwalteterBargeldbetrag.
class Geldboerse(VerwalteterBargeldbetrag):
# TODO: Spezielle Methoden fuer eine Geldboerse
pass
class Tresor(VerwalteterBargeldbetrag):
# TODO: Spezielle Methoden fuer einen Tresor
pass
Mit den beiden Kommentaren soll angedeutet werden, dass an dieser Stelle noch Methoden fehlen, die eine Geldbörse und einen Tresor zu besonderen verwalteten Geldbeträgen machen. Da wir an dieser Stelle keine vollständige Software entwickeln, sondern Ihnen die prinzipielle Erweiterbarkeit des Programms demonstrieren möchten, verzichten wir auf diese Details. Sie können sich als Übung einmal selbst überlegen, welche Funktionalität in den beiden Fällen sinnvoll ist.
Die Klassen Girokonto, Nummernkonto und NummernkontoMitTagesumsatz
Die Klasse Girokonto erbt direkt von der Klasse AllgemeinesKonto.
class Girokonto(AllgemeinesKonto):
def __init__(self, inhaber, kontonummer, kontostand):
kundendaten = GirokontoKundendaten(inhaber, kontonummer)
super().__init__(kundendaten, kontostand)
Analog zur Klasse GirokontoKundendaten führen wir die Klasse NummernkontoKundendaten ein, um die Kundendaten eines Nummernkontos zu verwalten. In unserem Modell wird ein Nummernkonto durch eine Identifikationsnummer beschrieben.
class NummernkontoKundendaten:
def __init__(self, identifikationsnummer):
self.Identifikationsnummer = identifikationsnummer
def zeige(self):
print("Identifikationsnummer:", self.Identifikationsnummer)
Mithilfe dieser Klasse können wir die Klassen Nummernkonto und NummernkontoMitTagesumsatz definieren.
class Nummernkonto(AllgemeinesKonto):
def __init__(self, identifikationsnummer, kontostand):
kundendaten = NummernkontoKundendaten(identifikationsnummer)
super().__init__(kundendaten, kontostand)
class NummernkontoMitTagesumsatz(AllgemeinesKontoMitTagesumsatz):
def __init__(self, kontonummer, kontostand, max_tagesumsatz):
kundendaten = NummernkontoKundendaten(kontonummer)
super().__init__(kundendaten, kontostand, max_tagesumsatz)
Zur Demonstration verwenden wir die beiden Klassen in einem kleinen Beispielprogramm.
>>> nk1 = Nummernkonto(113427613185, 5000)
>>> nk2 = NummernkontoMitTagesumsatz(45657364234, 12000, 3000)
>>> nk1.auszahlen(1000)
True
>>> nk2.einzahlen(1500)
True
>>> nk1.geldtransfer(nk2, 2000)
False
>>> nk1.zeige()
Identifikationsnummer: 113427613185
Betrag: 4000.00
>>> nk2.zeige()
Identifikationsnummer: 45657364234
Betrag: 13500.00
Heute schon 1500.00 von 3000.00 Euro umgesetzt
Es werden sowohl eine Instanz der Klasse Nummernkonto als auch der Klasse NummernkontoMitTagesumsatz erzeugt. Anschließend werden von dem ersten Konto 1.000 € abgehoben und 1.500 € auf das zweite eingezahlt. Schließlich versuchen wir, 2.000 € von dem Konto nk1 auf das Konto nk2 zu überweisen. Da der Tagesumsatz von nk2 damit überschritten würde, schlägt dies fehl.
Wie die Ausgabe zeigt, arbeiten die beiden Klassen genauso wie die anderen Kontoklassen.
21.2.4 Ausblick 

Der große Vorteil der Vererbung ist, dass man aus vorhandenen Klassen neue Klassen ableiten kann, um diese dann an die zu lösende Problemstellung anzupassen. Dabei kann die abgeleitete Klasse auf die gesamte Funktionalität zurückgreifen, die von der Basisklasse zur Verfügung gestellt wird. Folglich müssen nur noch die Methoden implementiert bzw. überschrieben werden, die nicht zur neuen Problemstellung passen.
Würden wir beispielsweise ausgehend von der Klasse Konto die Klassen Girokonto, Nummernkonto, GirokontoMitTagesumsatz und NummernkontoMitTagesumsatz entwickeln, ohne auf Vererbung zurückzugreifen, müssten wir die Methoden zum Ein- und Auszahlen in jeder dieser Klassen neu implementieren. Dies hätte dazu geführt, dass an mehreren Stellen unseres Programms sehr ähnlicher Code stehen würde. Diese Dopplung von Code bläht den Umfang eines Programms unnötig auf. Dadurch werden Wartung und Weiterentwicklung erschwert, da immer an mehreren Stellen parallel gearbeitet bzw. korrigiert werden muss.
Durch geschickte Strukturierung mittels Vererbung sind Programme möglich, die mit einem Minimum an Funktionalitätsdopplung auskommen.
In großen Softwareprojekten haben wir es nicht wie in unserem Modellbeispiel mit einer Handvoll Klassen zu tun, sondern es kommen Hunderte oder Tausende Klassen zum Einsatz. In einem solchen Umfeld fallen die durch Vererbung gemachten Einsparungen noch deutlicher ins Gewicht.
21.2.5 Mehrfachvererbung 

Bisher haben wir eine Subklasse immer von genau einer Basisklasse erben lassen. Es gibt aber Situationen, in denen eine Klasse die Fähigkeiten von zwei oder noch mehr Basisklassen erben soll, um das gewünschte Ergebnis zu erzielen. Dieses Konzept, bei dem eine Klasse von mehreren Basisklassen erbt, wird Mehrfachvererbung genannt.
Möchten Sie eine Klasse von mehreren Basisklassen erben lassen, schreiben Sie die Basisklassen durch Kommata getrennt in die Klammern hinter den Klassennamen:
class NeueKlasse(Basisklasse1, Basisklasse2, Basisklasse3):
# Definition von Methoden und Attributen
pass
In diesem Beispiel erbt die Klasse NeueKlasse von den drei Klassen Basisklasse1, Basisklasse2 und Basisklasse3.
Mehrfachvererbung ist ein sehr komplexes Thema, weshalb wir uns hier nur auf ein abstraktes Beispiel beschränken möchten, um Ihnen die dahinterstehende Idee zu verdeutlichen.
Wir nehmen an, wir hätten zwei Klassen zur Beschreibung von Geländefahrzeugen und Wasserfahrzeugen, nämlich Gelaendefahrzeug und Wasserfahrzeug. Wenn wir nun eine Klasse Amphibienfahrzeug definieren möchten, kommen sowohl die Klasse Gelaendefahrzeug als auch die Klasse Wasserfahrzeug als Basisklasse infrage, denn ein Amphibienfahrzeug ist sowohl das eine als auch das andere.
Es ist daher nur konsequent, die Klasse Amphibienfahrzeug von beiden dieser Klassen erben zu lassen, wie es Abbildung 21.4 veranschaulicht.
Abbildung 21.4 Mehrfachvererbung am Beispiel eines Amphibienfahrzeugs
Im Ergebnis erbt die Klasse Amphibienfahrzeug die Methoden beider Klassen Gelaendefahrzeug und Wasserfahrzeug.
Mögliche Probleme der Mehrfachvererbung
Es ist kein Zufall, dass nur wenige Sprachen das Konzept der Mehrfachvererbung unterstützen, da es eine Reihe prinzipieller Probleme gibt.
Beispielsweise kommt es vor, dass mehrere Basisklassen eine Methode mit dem gleichen Namen implementieren. Die erbende Klasse erbt diese Methode dann von derjenigen Basisklasse, die am weitesten links in der Liste der Basisklassen steht.
Nun müssen zwei Methoden mit demselben Namen aber keinesfalls die gleiche Aufgabe erfüllen. Im schlimmsten Fall kann es also passieren, dass die erbende Klasse unbenutzbar wird, weil sie nur eine der in Konflikt stehenden Methoden erben kann.
In der Praxis lässt sich Mehrfachvererbung in der Regel umgehen, weshalb wir hier nicht näher darauf eingehen.