Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
1 Einleitung
2 Die Programmiersprache Python
Teil I Einstieg in Python
3 Erste Schritte im interaktiven Modus
4 Der Weg zum ersten Programm
5 Kontrollstrukturen
6 Dateien
7 Das Laufzeitmodell
8 Funktionen, Methoden und Attribute
9 Informationsquellen zu Python
Teil II Datentypen
10 Das Nichts – NoneType
11 Operatoren
12 Numerische Datentypen
13 Sequenzielle Datentypen
14 Zuordnungen
15 Mengen
16 Collections
17 Datum und Zeit
18 Aufzählungstypen – Enum
Teil III Fortgeschrittene Programmiertechniken
19 Funktionen
20 Modularisierung
21 Objektorientierung
22 Ausnahmebehandlung
23 Iteratoren
24 Kontextobjekte
25 Manipulation von Funktionen und Methoden
Teil IV Die Standardbibliothek
26 Mathematik
27 Kryptografie
28 Reguläre Ausdrücke
29 Schnittstelle zu Betriebssystem und Laufzeitumgebung
30 Kommandozeilenparameter
31 Dateisystem
32 Parallele Programmierung
33 Datenspeicherung
34 Netzwerkkommunikation
35 Debugging und Qualitätssicherung
36 Dokumentation
Teil V Weiterführende Themen
37 Anbindung an andere Programmiersprachen
38 Distribution von Python-Projekten
39 Grafische Benutzeroberflächen
40 Python als serverseitige Programmiersprache im WWW – ein Einstieg in Django
41 Wissenschaftliches Rechnen
42 Insiderwissen
43 Von Python 2 nach Python 3
A Anhang
Stichwortverzeichnis

Download:
- Beispielprogramme, ca. 464 KB

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Python 3 von Johannes Ernesti, Peter Kaiser
Das umfassende Handbuch
Buch: Python 3

Python 3
Pfeil 21 Objektorientierung
Pfeil 21.1 Klassen
Pfeil 21.1.1 Definieren von Methoden
Pfeil 21.1.2 Der Konstruktor und die Erzeugung von Attributen
Pfeil 21.2 Vererbung
Pfeil 21.2.1 Technische Grundlagen
Pfeil 21.2.2 Die Klasse GirokontoMitTagesumsatz
Pfeil 21.2.3 Mögliche Erweiterungen der Klasse Konto
Pfeil 21.2.4 Ausblick
Pfeil 21.2.5 Mehrfachvererbung
Pfeil 21.3 Setter und Getter und Property Attributes
Pfeil 21.3.1 Setter und Getter
Pfeil 21.3.2 Property-Attribute
Pfeil 21.4 Klassenattribute und Klassenmethoden sowie statische Methoden
Pfeil 21.4.1 Statische Methoden
Pfeil 21.4.2 Klassenmethoden
Pfeil 21.4.3 Klassenattribute
Pfeil 21.5 Built-in Functions für Objektorientierung
Pfeil 21.5.1 Funktionen für die Verwaltung der Attribute einer Instanz
Pfeil 21.5.2 Funktionen für Informationen über die Klassenhierarchie
Pfeil 21.6 Objektphilosophie
Pfeil 21.7 Magic Methods und Magic Attributes
Pfeil 21.7.1 Allgemeine Magic Methods
Pfeil 21.7.2 Operatoren überladen
Pfeil 21.7.3 Datentypen emulieren
 
Zum Seitenanfang

21.2    Vererbung Zur vorigen ÜberschriftZur nächsten Überschrift

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.

 
Zum Seitenanfang

21.2.1    Technische Grundlagen Zur vorigen ÜberschriftZur nächsten Überschrift

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.

 
Zum Seitenanfang

21.2.2    Die Klasse GirokontoMitTagesumsatz Zur vorigen ÜberschriftZur nächsten Überschrift

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:

  1. Daten, die den Umgang mit dem Geld auf dem Konto betreffen (Kontostand, MaxTagesumsatz, UmsatzHeute)
  2. 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.

 
Zum Seitenanfang

21.2.3    Mögliche Erweiterungen der Klasse Konto Zur vorigen ÜberschriftZur nächsten Überschrift

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.

Die Klassenhierarchie des Kontobeispiels

Abbildung 21.2    Die Klassenhierarchie des Kontobeispiels

Eine erweiterte 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.

 
Zum Seitenanfang

21.2.4    Ausblick Zur vorigen ÜberschriftZur nächsten Überschrift

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.

 
Zum Seitenanfang

21.2.5    Mehrfachvererbung  Zur vorigen ÜberschriftZur nächsten Überschrift

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.

Mehrfachvererbung am Beispiel eines Amphibienfahrzeugs

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.

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Python 3 Python 3
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Einstieg in Python
Einstieg in Python


Zum Rheinwerk-Shop: Python. Der Grundkurs
Python. Der Grundkurs


Zum Rheinwerk-Shop: Algorithmen mit Python
Algorithmen mit Python


Zum Rheinwerk-Shop: Objektorientierte Programmierung
Objektorientierte Programmierung


Zum Rheinwerk-Shop: Raspberry Pi. Das umfassende Handbuch
Raspberry Pi. Das umfassende Handbuch


Zum Rheinwerk-Shop: Roboter-Autos mit dem Raspberry Pi
Roboter-Autos mit dem Raspberry Pi


Zum Rheinwerk-Shop: Neuronale Netze programmieren mit Python
Neuronale Netze programmieren mit Python


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2020
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern