Python ist eine objektorientierte Programmiersprache. Deshalb wurden einige Aspekte der objektorientierten Programmierung bereits an mehreren Stellen des Buches erwähnt. Das ließ sich gar nicht vermeiden. Sie haben Objekte wie Listen oder Strings verwendet und Methoden von Objekten aufgerufen. Aber Ihre Programme waren noch nicht objektorientiert.
Dieses Kapitel geht einen Schritt weiter und erklärt, wie man objektorientierte Software gestaltet. Sie erfahren, wie man ...
Objektorientierung ist eine sehr mächtige Methode der strukturellen Zerlegung, denn ein umfangreiches Programm kann in relativ große, in sich zusammenhängende Teile gegliedert werden, die ihrerseits noch weiter unterteilt werden können. Deshalb sind moderne, professionelle Programme objektorientiert. Eine objektorientierte Modellierung beginnt mit dem Entwurf einer Klassenstruktur. Dieser Abschnitt beginnt etwas philosophisch und stellt zunächst die wesentlichen Grundideen der Objektorientierten Programmierung (OOP) vor.
Die Grundideen der OOP kann man in drei Punkten zusammenfassen.
Einfache Programme folgen einem sogenannten imperativen Programmierstil. Man stellt sich vor, dass es nur einen Akteur gibt, der den Programmlauf kontrolliert. Man stellt sich vor, dass der Computer (oder der Python-Interpreter) Befehlen gehorcht und Anweisungen ausführt. Dagegen gibt es in einem objektorientierten Programm mehrere Akteure: Objekte, die untereinander Botschaften austauschen und gemeinsam eine Aufgabe lösen.
Ein Objekt fasst Daten (Attribute) und Operationen (Methoden) zu einer aktiven Einheit zusammen. Attribute (oder Objektvariablen) sind die Merkmale oder Eigenschaften eines Objekts. Sie werden mit Werten belegt. Ein Objekt befindet sich immer in einem bestimmten Zustand. Der momentane Zustand ist durch Belegungen der Attribute definiert. Bei Python gibt es änderbare und nicht änderbare Objekte. Nicht änderbare Objekte sind z.B. Zahlen oder Strings. Sie befinden sich immer in demselben Zustand. Änderbare Objekte sind z.B. Listen. Wenn man eine Liste um ein Element verlängert, ändert man ihren Zustand. Die Operationen, die Objekte ausführen können, nennt man Methoden.
Zu einer Klasse gehören Objekte mit gleichen Eigenschaften (Attributen), die die gleichen Operationen beherrschen. Man kann sich eine Klasse als Bauplan für Objekte vorstellen. In der Klasse sind Attribute und Methoden festgelegt.
Ein objektorientiertes Programmierprojekt beginnt mit dem Entwurf einer Klassenstruktur. Eine Klasse definiert einen Typ von Objekten. Klassen sind Modelle von Dingen, Ereignissen oder Ideen der Realwelt.
Als Beispiel entwickeln wir eine Klasse, die Flaschen modelliert. Der Name der Klasse ist deshalb Flasche. Klassennamen beginnen üblicherweise mit einem Großbuchstaben. Die Objekte einer Klasse besitzen Attribute und Methoden. Das erste Attribut der Flasche nennen wir inhalt. Es ist das Volumen einer Flüssigkeit in cm³, mit der die Flasche gefüllt ist. Eine Flasche hat ein bestimmtes Fassungsvermögen, also einen maximalen Inhalt, der nicht überschritten werden kann. Dieses Attribut nennen wir max_inhalt.
Nun kann eine Flasche geöffnet oder geschlossen sein. Diese Facette ihres Zustands soll durch ein boolesches Attribut namens geöffnet modelliert werden. Wenn geöffnet den Wert True trägt, ist die Flasche geöffnet. Ansonsten ist sie geschlossen.
Welche Methoden beherrscht eine Flasche? Sie kann sich öffnen und schließen. Sie kann sich mit einem bestimmten Volumen füllen, sofern sie geöffnet ist. Und sie kann sich leeren, sofern sie geöffnet ist.
Im Unterschied zur Realität sind in der objektorientierten Denkweise die Objekte eigenaktiv und ändern selbst ihren Zustand. Man stellt sich vor, dass ein Objekt der Klasse Flasche sich selbst öffnet, wenn es den Auftrag dazu erhält.
Die Überlegungen zu Attributen und Methoden kann man in einem UML-Klassendiagramm wie in Abbildung 13.1 zusammenfassen (UML: Unified Modeling Language).
Abb. 13.1: UML-Klassendiagramm
Ein UML-Klassendiagramm ist die Grundlage für die Entwicklung von Python-Programmtext.
Eine Klassendefinition hat in einfachen Fällen folgenden Aufbau:
class Klassenname:
Docstring
def __init__(self, ...):
Anweisungen zur Initialisierung
def methode_1(self, ...)
Anweisungen
...
Die Klassendefinition beginnt mit dem Schlüsselwort class. Danach kommt der Name der Klasse und am Ende der Zeile ein Doppelpunkt. Wie immer folgt nach einem Doppelpunkt ein neuer Block, der eingerückt ist. In der ersten Zeile dieses Blocks kann – wie bei Funktionsdefinitionen – ein Docstring stehen, der die Klasse kurz erklärt. Darunter folgen die Methodendefinitionen. Sie sind wie Funktionsdefinitionen aufgebaut. Allerdings beginnt jede Parameterliste mit einem speziellen Parameter, den man üblicherweise self nennt. Der Parameter self bezeichnet das aktuelle Objekt, in diesem Fall ein konkretes Objekt der Klasse Flasche. Statt self könnte man auch einen beliebigen anderen Namen verwenden (z.B. this), aber es ist eine sehr starke Konvention, den Namen self zu verwenden.
Die erste Methode hat den vorgegebenen Namen __init__(). Diese Methode nennt man auch Initialisierungsmethode. Sie wird automatisch aufgerufen, wenn man ein Objekt der Klasse erzeugt (dazu später mehr). In der Initialisierungsmethode werden die Attribute der Objekte der Klasse definiert und mit Anfangswerten belegt. Beachten Sie, dass dem Namen eines Objekt-Attributs immer self. vorangestellt wird.
class Flasche: #1
'Modell einer Flasche' #2
def __init__ (self): #3
self.inhalt = 0 #4
self.max_inhalt = 1000
self.geöffnet = False
def öffnen(self):
self.geöffnet = True #5
def schließen(self):
self.geöffnet = False
def füllen(self, volumen):
if self.geöffnet: #6
if self.inhalt + volumen <= self.max_inhalt:
self.inhalt += volumen #7
def leeren(self):
if self.geöffnet:
self.inhalt = 0 #8
#1: Kopfzeile der Klassendefinition
#2: Docstring
#3: Initialisierungsmethode. Sie wird aufgerufen, wenn die Klasse aufgerufen und ein neues Objekt erzeugt wird.
#4: Das Attribut inhalt wird definiert und erhält den Anfangswert 0.
#5: Das Attribut geöffnet erhält den Wert True. Die Flasche ist jetzt geöffnet.
#6: Nur wenn die Flasche geöffnet ist, kann sie sich befüllen.
#7: Wenn das Volumen volumen noch in die Flasche passt, wird es dem Inhalt hinzugefügt. Das Attribut inhalt bekommt dann einen neuen Wert.
#8: Die Methode leeren() ist hier so definiert, dass die Flasche sich vollständig entleert.
Eine Klasse kann man sich als Bauplan für Objekte vorstellen. Um ein Objekt der Klasse zu erzeugen, muss man die Klasse aufrufen. In der folgenden Anweisung entsteht ein neues Objekt der Klasse Flasche. Das neue Objekt hat den Namen a.
a = Flasche()
Diesen Vorgang nennt man instanziieren und das neue Objekt nennt man auch Instanz der Klasse. Beim Instanziieren wird die Methode __init__() aufgerufen, die die Anfangswerte der Attribute festlegt.
objekt.attribut
können Sie auf ein Attribut eines Objekts zugreifen. Im Beispiel der Klasse Flasche liefert die Anweisung
print(a.max_inhalt)
die Ausgabe
1000
Hier hat das Objekt den Namen a und das Attribut den Namen max_inhalt.
Eine Methode wird in folgendem Format aufgerufen:
objekt.methode(...)
Wichtig ist, dass beim Aufruf das erste Argument self der Methodendefinition weggelassen wird.
Beispiel:
a = Flasche()
a.öffnen()
a.füllen(100)
print(a.inhalt)
Ausgabe:
100
Welche Ausgaben liefert die folgende Anweisungsfolge?
a = Flasche()
a.füllen(500)
print(a.inhalt)
b = Flasche()
b.öffnen()
b.füllen(500)
b.füllen(1000)
print(b.inhalt)
Objekte einer Klasse haben immer die gleichen Attribute. Aber die Belegung der Attribute bei der Instanziierung kann sich unterscheiden. Zum Beispiel gibt es Flaschen mit unterschiedlichem Fassungsvermögen. Will man das in die Klassendefinition einbeziehen, muss man die Definition der Initialisierungsmethode etwas abwandeln und einen zusätzlichen Parameter einsetzen:
def __init__ (self, fassungsvermögen): #1
self.inhalt = 0
self.max_inhalt = fassungsvermögen #2
self.geöffnet = False
#1: Die Methode __init__() erhält einen weiteren Parameter für die Belegung des Attributs max_inhalt.
#2: Dem Attribut max_inhalt wird der Wert des neuen Parameters fassungsvermögen zugewiesen.
Bei der Instanziierung eines Objekts der veränderten Klasse wird dann als Argument eine Zahl für das Fassungsvermögen angegeben, z.B.
kleine_flasche = Flasche(200)
Die Klasse Flasche ist ein Modell für einen Behälter, der Flüssigkeiten oder Gase aufnehmen kann. Eine solche Klasse (wenn man sie noch ein wenig weiterentwickelt) könnte man in einem Programm verwenden, das die Befüllung eines Schwimmbads, Öltanks oder eines Gasometers steuert. Wenn dann die Klasse immer noch Flasche heißt, wird der Begriff Flasche als Metapher verwendet. Das Modellieren einer Flasche fällt uns leicht, weil uns Flaschen aus dem Alltag vertraut sind. In der Programmierung werden häufig Metaphern verwendet, um Programmtext verständlicher zu machen. Denken Sie an die Klassen aus dem Modul tkinter: Label (Etikett) oder Button (Schaltknopf).
Geld spielt in unserer Welt eine wichtige Rolle. Jeden Tag hantiert man mit Münzen, Scheinen, Rechnungen, Briefmarken oder Überweisungsaufträgen. In diesem Abschnitt entwickeln wir eine Klasse, die Geldbeträge modelliert und das Rechnen mit Geld unterstützt.
Ein Geldbetrag besteht immer aus einer Zahl und einer Währung. Wenn man Geldbeträge unterschiedlicher Währungen addiert, muss man die aktuellen Wechselkurse beachten. In der folgenden Klassendefinition ist der Wechselkurs einiger Währungen durch ein Dictionary namens wechselkurs definiert. Schlüssel sind die internationalen Abkürzungen für Währungen. Die zugeordneten Werte geben den Preis in Euro an, den man für eine Währungseinheit zahlen muss (Stand 1.1.2021). Beispielsweise hat wechselkurs['USD'] den Wert 0.8154. Das bedeutet: Ein US-Dollar kostet 0.8154 Euro.
Das Dictionary wechselkurs ist ein sogenanntes Klassenattribut. Es wird in Zeile #1 außerhalb einer Methodendefinition definiert. Beachten Sie: Dem Namen des Klassenattributs wird kein self. vorangestellt. Ein Klassenattribut gilt für alle Objekte der Klasse.
class Geld:
'Die Klasse modelliert einen Geldbetrag'
wechselkurs ={'USD':0.8154,
'GBP':1.1129,
'EUR':1.0,
'JPY':0.0079} #1
def __init__(self, währung, betrag):
self.währung = währung #2
self.betrag = float(betrag) #3
def berechneEuro(self): #4
return self.betrag * self.wechselkurs[self.währung]
def add (self, geld): #5
a = self.berechneEuro() #6
b = geld.berechneEuro()
summe = (a + b)/self.wechselkurs[self.währung] #7
return Geld(self.währung, summe) #8
#1: Das Dictionary wechselkurs ist ein Klassenattribut. Es gilt für alle Objekte dieser Klasse.
#2: Hier erhalten die Objekt-Attribute die Werte, die bei der Instanziierung als Argumente übergeben worden sind.
#3: Durch Aufruf von float() wird sichergestellt, dass das Attribut betrag eine Zahl enthält.
#4: Die Methode gibt den Wert des Geld-Objekts in Euro zurück. Sie wird intern für Berechnungen verwendet.
#5: Die Methode nimmt ein Geld-Objekt als Parameter, addiert den Wert dieses Geld-Objekts zum eigenen Wert und gibt ein neues Geld-Objekt mit der Summe zurück.
#6: Im ersten Schritt werden die beiden zu addierenden Geld-Beträge in Euro umgerechnet.
#7: Die beiden Geldbeträge in Euro werden addiert und dann in die Währung des Objekts self umgerechnet. Das Ergebnis ist die Summe der beiden Geldbeträge.
#8: Hier wird durch den Aufruf von Geld() ein neues Geldobjekt erzeugt. Es repräsentiert die Summe der beiden Geldbeträge und hat die gleiche Währung wie das Objekt self, das die Botschaft erhalten hat.
Wenn Sie die Programmdatei mit der Definition der Klasse Geld speichern und unter IDLE mit Taste F5 ausführen, können Sie anschließend in der Python-Shell einige Experimente machen.
Instanziieren Sie ein Objekt der Klasse Geld:
>>> a = Geld('EUR', 12)
Den Wert eines Geld-Objekts kann man nicht direkt durch eine print()-Anweisung ausgeben:
>>> print(a)
<__main__.Geld object at 0x0000026B5F9E7070>
Um den Wert des neuen Objekts auszugeben, müssen Sie auf die Attribute währung und betrag zugreifen:
>>> print(a.währung, a.betrag)
EUR 12
Um das Geld-Objekt a mit einem anderen Geld-Objekt zu addieren, rufen Sie die Methode add() auf:
>>> b = a.add(Geld('USD', 1))
>>> print(b.währung, b.betrag)
EUR 12.8154
Es funktioniert, aber es sieht ein wenig umständlich aus. Sie werden später einen Weg kennenlernen, Grundoperationen wie Ausgabe und Addition eleganter – also besser lesbar – zu formulieren.
Ein Klassenattribut beschreibt eine Eigenschaft, die für alle Objekte einer Klasse gilt. Es wird zu Beginn einer Klassendefinition im Format
klassenattribut = wert
definiert. Im Unterschied zu Objektattributen wird dem Namen des Klassenattributs kein self. vorangestellt. Auf ein Klassenattribut können Sie mit einem Ausdruck der Form
klasse.klassenattribut
zugreifen. Es wird also der Name der Klasse und nicht der Name eines Objekts verwendet.
Beispiel:
>>> Geld.wechselkurs
{'USD': 0.8154, 'GBP': 1.1129, 'EUR': 1.0, 'JPY': 0.0079}
Python ist objektorientiert. Dennoch kann Python arithmetische Ausdrücke wie a + 2 auswerten:
>>> a = 10
>>> a + 2
12
Der Operator + steht für eine Additionsmethode, die Objekte vom Typ int beherrschen. Sie können die Addition auch im Stil einer Botschaft an ein Objekt der Klasse int formulieren. Probieren Sie das aus:
>>> a = 10
>>> a.__add__(2)
12
In der Definition der Klasse int ist eine Methode namens __add__() definiert, die die Addition bewerkstelligt. Sie wird intern vom Python-Interpreter aufgerufen, wenn er einen Ausdruck mit dem Operator + auswertet.
Nun kann man den Operator + auch auf Objekte anderer Typen anwenden, z.B. Gleitkommazahlen (float) und Zeichenketten (str). Die Klassendefinitionen von float und str besitzen ebenfalls Definitionen der Methode
__add__(), die jeweils die Addition auf unterschiedliche Weise implementieren (umsetzen).
Die Methoden wie __add__(), deren Namen mit doppelten Unterstrichen beginnen und enden, nennt man auch »magische Methoden«, weil sie auf »magische Weise« die Arbeit des Python-Interpreters beeinflussen.
Eine zweite magische Methode ist __str__(). Sie legt fest, wie ein Objekt der Klasse in einer print()-Anweisung dargestellt werden soll.
Wenn man für eine Klasse die Methode __add__() definiert, hat man den Operator + überladen. Diese Technik, für ein und denselben Operator in unterschiedlichen Klassen unterschiedliche Methoden zu definieren, nennt man Polymorphie (griechisch: polymorphia für Vielgestaltigkeit).
Das folgende Programm enthält eine Variante der Klasse Geld, in der __add__() und __str__() verwendet werden.
Diese Klassendefinition macht es möglich, dass Geld-Objekte wie Zahlen in einfachen arithmetischen Ausdrücken wie
a + b
addiert werden können und in print()-Anweisungen direkt als lesbarer Text ausgegeben werden können.
Abbildung 13.2 zeigt das UML-Klassensymbol mit den Attributen und Methoden der veränderten Klasse Geld. Klassenattribute werden in UML-Klassensymbolen unterstrichen. Beachten Sie, dass die Methode __init__() weggelassen wurde.
Abb. 13.2: UML-Klassensymbol für die veränderte Klasse Geld.
Das Klassenattribut wird unterstrichen.
# geld.py
class Geld:
'Die Klasse modelliert Geldbeträge'
wechselkurs ={'USD':0.8154,
'GBP':1.1129,
'EUR':1.0,
'JPY':0.0079}
def __init__(self, währung, betrag):
self.währung = währung
self.betrag = float(betrag)
def berechneEuro(self):
return self.betrag*self.wechselkurs[self.währung]
def __add__ (self, geld): #1
a = self.berechneEuro()
b = geld.berechneEuro()
summe = (a + b)/self.wechselkurs[self.währung]
return Geld(self.währung, summe)
def __str__(self): #2
return '{} {}'.format(self.währung,
round(self.betrag, 2)) #3
if __name__ == '__main__': #4
a = Geld('EUR', 10)
b = Geld('USD', 1)
print(a + b) #5
#1: Die Methode bekommt einen neuen Namen, aber der Rest der Definition bleibt gleich. Durch den Namen __add__() wird der Plusoperator + überladen.
#2: Die Methode __str__() liefert eine lesbare Repräsentation eines Objekts dieser Klasse.
#3: Hier wird aus den Attributwerten ein String konstruiert. Die Zeichenkette '{} {}' ist ein Formatstring mit zwei Platzhaltern. Der erste Platzhalter wird durch die Währung des Objekts und der zweite Platzhalter durch den auf zwei Stellen nach dem Komma gerundeten Betrag ersetzt.
#4: Am Ende des Programmtextes sind einige Anweisungen zum Testen der Klasse. Sie werden nur ausgeführt, wenn die Programmdatei direkt gestartet wird.
#5: Hier werden gleich zwei Dinge getestet:
1. Funktioniert das Überladen des Plusoperators durch __add__()?
2. Wird von __str__() eine gut lesbare Darstellung des Geld-Objekts geliefert, das die Summe der beiden Geld-Objekte a und b darstellt?
EUR 10.82
Auch für andere Operatoren und Standardfunktionen gibt es reservierte Methodennamen. Tabelle 13.1 beschreibt einige Beispiele.
Tab. 13.1: Einige reservierte Methodennamen mit doppelten Unterstrichen
Schreiben Sie für die Klasse Geld Methoden, die die Operatoren > und * überladen. Mit der erweiterten Klasse Geld sollen folgende Anweisungen möglich sein:
>>> a = Geld('EUR', 2)
>>> b = Geld('GBP', 2)
>>> print(a * 10)
20.00 EUR
>>> a < b
True
Das Projekt Abrechnung ist ein praktisches Beispiel, wie man Objekte der Klasse Geld nutzen kann.
Im Anwendungsfenster (Abbildung 13.3) sieht man ein Textfeld und eine Schaltfläche. In das Textfeld trägt man untereinander Geldbeträge mit eventuell unterschiedlichen Währungen ein. Klickt man auf die Schaltfläche Abrechnen, erscheint unten im Textfeld die Summe der Beträge in Euro.
Abb. 13.3: Die GUI des Projekts besteht aus einem Textfeld für die Eingabe der Beträge und die Ausgabe des Ergebnisses und einer Schaltfläche.
Abbildung 13.4 zeigt die Klassenstruktur des Programms. Das UML-Diagramm besteht aus Klassensymbolen und verbindenden Symbolen, sogenannten Assoziationen. Sie bringen Beziehungen zwischen den Objekten der Klassen zum Ausdruck. In diesem Fall ist die Beziehung zwischen der Klasse App und den anderen Klassen eine Aggregation, dargestellt durch eine Verbindungslinie mit einer offenen Raute an einem Ende. Aggregationen sind die Beziehung zwischen einem Ganzen (Aggregat) und den Teilen, aus denen es zusammengesetzt ist. In diesem Fall kann man das UML-Klassendiagramm so lesen: Ein Objekt der Klasse App besteht aus Objekten der Klassen Geld, Tk, Text und Button.
Abb. 13.4: UML-Diagramm mit Klassensymbolen und Assoziationen
Viele Objekte des Alltags sind Aggregate:
Das UML-Diagramm in Abbildung 13.4 ist die Grundlage für das folgende Python-Programm.
# abrechnung.pyw
from geld import Geld #1
from tkinter import Button, Tk, Text, LEFT, END
class App():
def __init__(self):
self.fenster = Tk()
self.text = Text(master=self.fenster,
width=30, height=6) #2
self.button = Button(master=self.fenster,
text='Abrechnen',
command=self.abrechnen)
self.text.pack()
self.button.pack(side=LEFT, padx=5, pady=5)
self.fenster.mainloop()
def abrechnen(self):
text = self.text.get(1.0, END) #3
zeilen = text.split('\n') #4
summe = Geld('EUR', 0) #5
for z in zeilen: #6
try:
währung, betrag = z.split() #7
summe = summe + Geld(währung, betrag) #8
except:
pass
self.text.insert(END,
'\n\nSumme: ' + str(summe)) #9
App() #10
Erläuterungen:
#1: Aus dem Modul geld wird die Klasse Geld importiert.
#2: Textfeld für 6 Zeilen der Länge 30 zur Ein- und Ausgabe von Text
#3: Aus dem Textfeld wird der gesamte Text gelesen und als String der Variablen text zugewiesen.
#4: Aus dem Text wird eine Liste von Strings erzeugt und der Variablen zeilen zugewiesen. Der Trennungsstring ist das Sonderzeichen (Escapesequenz) für einen Zeilenumbruch. Die Variable zeilen enthält also eine Liste von Textzeilen.
#5: Es wird ein neues Geld-Objekt erzeugt, das 0 Euro repräsentiert. Es bekommt den Namen summe. In dieser Variablen werden die Beträge der Rechnung aufsummiert.
#6: Es werden nun die eingegebenen Zeilen durchlaufen und jede Zeile versuchsweise (in einer try-Klausel) ausgewertet. Wenn in der Zeile keine gültige Angabe steht, macht das nichts. Dann wird diese Zeile einfach übersprungen.
#7: Die Zeile z wird in zwei Teile aufgespalten (Trennstring ist leerer Raum). Der erste Teil (eine Währungsangabe) wird der Variablen währung zugewiesen und der zweite Teil (eine Zahl) der Variablen betrag.
#8: Es wird ein neues Geld-Objekt erzeugt, das den Geldbetrag in der Textzeile z repräsentiert. Dieses Geld-Objekt wird zum Geld-Objekt summe hinzuaddiert. Weil summe in dem arithmetischen Ausdruck zuerst steht, bleibt summe ein Objekt mit Währung EUR.
#9: Hinter das letzte Zeichen des Textes im Textfeld wird ein neuer String angefügt. Er beginnt mit zwei Zeilenumbrüchen. Dann kommt der Text Summe: und dahinter ein String, der das Geld-Objekt summe repräsentiert.
#10: Die Definition der Klasse App ist beendet. Jetzt – im Hauptprogramm – wird die Klasse App aufgerufen und so ein anonymes Objekt davon erzeugt. Es heißt anonym, weil es keinen Namen besitzt. Ein Name ist nicht erforderlich, weil niemals auf das App-Objekt zugegriffen wird. Es läuft sozusagen von alleine.
Vererbung beschreibt die Beziehung zwischen einer allgemeinen Klasse (Basisklasse) und einer spezialisierten Klasse (Unterklasse, abgeleitete Klasse). Die Unterklasse besitzt sämtliche Attribute und Methoden der Oberklasse. Man sagt, die Basisklasse vererbt ihre Merkmale. Darüber hinaus kann eine Unterklasse noch über zusätzliche Methoden und Attribute verfügen. Sie ist damit spezieller und weniger abstrakt.
Der Vorteil der Vererbung ist, dass geprüfte, fehlerfreie Software (die Basisklasse) unverändert in unterschiedlichen Programmen wiederverwendet werden kann.
Die Kopfzeile einer abgeleiteten Klasse hat folgendes Format:
class unterklasse(basisklasse):
Weitere Einzelheiten sollen nun an einem Beispiel erklärt werden. Die Klasse Geld aus dem letzten Abschnitt ist relativ allgemein. Sie passt zu allen möglichen Objekten, die einen monetären Wert darstellen. Wir verwenden Geld als Basisklasse für eine spezialisierte Klasse Zahlung zur Modellierung von Zahlungen. Objekte der Klasse Zahlung sollen in ihren Attributen neben Währung und Betrag auch den Zeitpunkt der Zahlung festhalten.
Abb. 13.5: UML-Diagramm mit Basisklasse und abgeleiteter Unterklasse
Abbildung 13.5 zeigt ein UML-Klassendiagramm, das die Klasse Zahlung beschreibt. Die Beziehung zwischen Basisklasse und Unterklasse wird durch einen Pfeil mit ungefüllter Spitze dargestellt. Im Klassensymbol der Unterklasse werden nur zusätzliche und geänderte (überschriebene) Methoden aufgeführt. In diesem Fall ist die Methode __str__() der Basisklasse, die eine lesbare Darstellung eines Geldobjekts zurückgibt, ungeeignet, weil sie kein Zahlungsdatum angibt. Sie wird deshalb in der Definition der abgeleiteten Klasse neu definiert (»überschrieben«). Dagegen können die Methoden __add__() und berechneEuro() der Basisklasse unverändert übernommen werden.
# zahlung.py
from geld import Geld #1
class Zahlung(Geld): #2
'Die Klasse modelliert eine Zahlung mit Datum'
def __init__(self, währung, betrag, zeitpunkt):
Geld.__init__(self, währung, betrag) #3
self.zeitpunkt = zeitpunkt #4
def __str__(self):
return "{} {} Datum: {}".format(self.währung,
round(self.betrag, 2),
self.zeitpunkt) #5
if __name__ == '__main__': #6
from time import asctime
a = Zahlung('EUR', 10, asctime()) #7
b = Geld('USD', 1)
print(a)
print(a + b) #8
EUR 10.0 Datum: Sat Jan 2 22:43:07 2021
EUR 10.82
#1: Aus dem Modul geld wird die Klasse Geld importiert.
#2: Die Klasse Zahlung wird von der Basisklasse Geld abgeleitet.
#3: Hier wird die Initialisierungsmethode der Basisklasse Geld aufgerufen. Die Attribute währung und geld werden als Argumente »weitergereicht«.
#4: Das zusätzliche Attribut zeitpunkt wird mit einem Wert belegt. Das kann z.B. ein String sein, der von der Funktion asctime() aus dem Modul time geliefert wird.
#5: Hier wird ein String konstruiert, der ein Zahlung-Objekt darstellt. Er enthält drei variable Teile: Währung, Betrag und den Zeitpunkt der Zahlung. Dieser String wird zurückgegeben.
#6: Das Hauptprogramm dient nur zum Testen der Klasse. Die if-Klausel wird nur ausgeführt, wenn die Programmdatei direkt aufgerufen wird, nicht aber beim Import.
#7: Hier wird ein Objekt der Klasse Zahlung instanziiert und dem Namen a zugewiesen. Das Objekt repräsentiert eine Zahlung von 10 Euro, die zum aktuellen Zeitpunkt erfolgt ist.
#8: Hier wird zu einem Zahlung-Objekt a ein Geld-Objekt b addiert und das Ergebnis ausgegeben. Die Addition ist in der Basisklasse definiert, funktioniert aber auch mit Zahlung-Objekten. Das Ergebnis der Addition ist aber immer ein Geld-Objekt, was ja auch sinnvoll ist. Denn in der Definition der Addition wird ein neues Geld-Objekt für die Summe erzeugt.
Abbildung 13.6 zeigt das UML-Klassendiagramm für eine Klasse, die Heilpflanzen modelliert.
Das Attribut name ist ein String mit dem Namen der Pflanze und wirkungen ist eine Liste aus Strings, die die Heilwirkungen der Pflanze bezeichnen.
Die Methode __gt__() überlädt den Vergleichsoperator > (größer). Wenn a und b Objekte der Klasse Heilpflanze sind, dann hat der Ausdruck a > b den Wahrheitswert True, wenn a mehr Wirkungen als b hat. Sonst hat a > b den Wahrheitswert False.
Abb. 13.6: UML-Diagramm der Klasse Heilpflanze
Die Methode hat_wirkung() prüft, ob ein Objekt eine bestimmte Wirkung hat. Wenn a ein Objekt der Klasse Heilpflanze ist, dann liefert der Aufruf
a.hat_wirkung(wirkung)
den Wert True, wenn wirkung in der Liste a.wirkungen enthalten ist. Sonst wird False zurückgegeben.
Formulieren Sie eine Klassendefinition zur Implementierung der Klasse Heilpflanze.
Ein Programm soll eine Sammlung von Sprüchen zu verschiedenen Anlässen modellieren. Zum Beispiel ist »Morgenstund' hat Gold im Mund.« ein Spruch für den Anlass »Frühsport« und »Je später der Abend, desto schöner die Gäste.« ein Spruch für den Anlass »Party«.
Abb. 13.7: UML-Klassendiagramm für eine Sammlung von Sprüchen
Die Klasse Sammlung enthält eine Liste von Objekten der Klasse Spruch. Im Programmtext fehlen noch die Implementierungen der Methoden neu(), suche() und anlässe().
Es sei s ein Objekt der Klasse Sammlung. Dann beschreibt Tabelle 13.2 die Wirkung der Methoden.
Tab. 13.2: Methoden der Klasse Sammlung
Ergänzen Sie im folgenden Programmtext die fehlenden Methodendefinitionen.
class Spruch:
'Modelliert einen Spruch'
def __init__(self, text, anlass):
self.text = text
self.anlass = anlass
class Sammlung:
'Modelliert eine Sammlung von Sprüchen'
def __init__(self):
self.sprüche = []
def neu(self, text, anlass):
pass
def suche(self, anlass):
pass
def anlässe(self):
pass
Die Ausgabe ist:
0
500
Erklärung: Die Flasche a muss erst geöffnet werden, bevor sie befüllt werden kann. Die Flasche b, die 500 cm³ enthält, kann sich nicht mit weiteren 1000 cm³ füllen, weil dadurch das Fassungsvermögen (Attribut max_inhalt) überschritten würde.
def __lt__(self, geld):
a = self.berechneEuro ()
b = geld.berechneEuro ()
return a < b
def __mul__(self, x):
ergebnis = self.betrag * x
return Geld(self.währung, ergebnis)
Beachten Sie, dass das erste Argument ein Objekt vom Typ Geld und das zweite Argument eine Zahl sein muss (Typ int oder Typ float).