5. Objektorientierte Programmierung#

5.1. Objektorientierte vs. Prozeduale Programmierung#

Angenommen wir möchten ein Programm für die Universitätsbibliothek schreiben, welche die registrierten Studierenden sowie die in der Bibliothek verfügbaren Lehrbücher organisiert. Schaue wir uns zunächst an, wie ein solches Programm in einem prozedualen Programmierstil aufgebaut sein könnte.

Die Studierenden beispielsweise besitzen verschiedene Eigenschaften, die wir in einer vorher definierten Reihenfolge in einer Liste ablegen könnten:

# Student: Name, Geschlecht, Alter, Studiengang, Semester, Noten
lisa  = ["Lisa", "w", 23, "Mathematik", 8, [1,3,2,2]]
bernd = ["Bernd", "m", 25, "Maschinenbau", 6, [3,3,4]]

Für Studierende könnte man nun freie Funktionen definieren, die mit diesen Eigenschaften zusammen einen logischen Programmablauf realisieren:

def celebrate_birthday(student):
    student[2] += 1

celebrate_birthday(lisa)
lisa
['Lisa', 'w', 24, 'Mathematik', 8, [1, 3, 2, 2]]

Dieser Programmierstil birgt allerdings einige Nachteile. Wenn wir im späteren Programmcode den Studiengang eines Studierenden wechseln wollen, werden wir uns noch daran erinnern, dass dieser an 4. Stelle der Liste steht? Außerdem könnte die freie Funktion celebrate_birthday auch auf alle anderen Listen mit mindestens 3 Elementen und einer Zahl an dritte Stelle angewendet werden. Unser Bibliotheksprogramm könnte die Lehrbücher beispielsweise wie folgt speichern

# Lehrbuch: Author, Titel, Regalstandort, Ausleihstatus
lehrbuch1 = ["Harro Heuser", "Lehrbuch der Analysis 1", 4, False]

und natürlich macht es keinen Sinn den Geburtstag einer Lehrbuchs zu feiern, dennoch würde unser Programm dies zulassen. Dann wird allerdings das Buch in das nächste Regal verschoben:

print("Unser Buch liegt in Regal", lehrbuch1[2])
celebrate_birthday(lehrbuch1)
print("Unser Buch liegt in Regal", lehrbuch1[2])
Unser Buch liegt in Regal 4
Unser Buch liegt in Regal 5

Sinnvoll wäre es hier die Funktion celebrate_birthday direkt an Studierende zu koppeln. Im folgenden Abschnitt schauen wir uns eine objektorientierte Umsetzung unseres Programms an.

5.2. Klassen, Methoden und Attribute#

Die objektorientierte Programmierung ist ein Programmierstil bei dem spezifische Eigenschaften und darauf anwendbare Operationen in individuellen Objekten gebündelt.

So ein Objekt ist beispielsweise ein Student oder eine Studentin, ausgestattet mit individuellen Eigenschaften wie Name, Geschlecht, Alter, Studiengang, Fachsemester, Notendurchschnitt. Eine Studentin führt gewisse Aktionen aus, beispielsweise Altern, Atmen, Lernen, eine Prüfung ablegen, etc..

Die Idee der Objektorientierten Programmierung ist es, die Klassifizierung Student/in mit einem eigens dafür vorgesehenen Datentyp, einer sogenannten Klasse, zu assozieren. Diese Klasse soll die vorher festgelegten Eigenschaften und Operationen bereitstellen. Wir erinnern uns vielleicht noch an die vorangegangenen Kapitel. Wir hatten bereits mit einer Klasse für komplexe Zahlen gearbeitet.

import cmath

c = 1+3j

# Abfrage eines Attributs
print("Realteil von c ist", c.real)

# Ausführen einer Methode
print("Die konjugiert komplexe lautet", c.conjugate())
Realteil von c ist 1.0
Die konjugiert komplexe lautet (1-3j)

Attribute und Methoden sind bereits die wichtigsten Bestandteile einer Klasse. Wir wollen nun lernen eigene Klassen zu implementieren. Die allgemeine Syntax sieht wie folgt aus:

class <class_name>:
    def __init__(self, <param1>, <param2>, ...):
        self.<attribute_1> = ...
        self.<attribute_2> = ...
    
    def <method_1>(self, [parameter_list]):
        [do something]
        return <result>
    
    def <method_2>(self, [parameter_list]):
        [do something]
        return <result>  

Die Klassendefinition beginnt mit dem Schlüsselwort class, gefolgt vom Namen unserer Klasse. Innerhalb der Klassendefinition werden nun Funktionen definiert, die wir in diesem Kontext als Methoden bezeichnen. Methoden sind stets an eine Instanz (Objekt) dieser Klasse gekoppelt. Eine spezielle Funktion, die wir hier zunächst hervorheben wollen ist die sogenannte Init-Funktion, auch Konstruktor genannt. Diese trägt immer den Namen __init__ und nimmt als Eingabeparameter das Objekt self entgegen, sowie beliebig viele weitere Parameter. Die Aufgabe der Init-Funktion ist es die Attribute der Klasse zu initialisieren. Die Attribute können beispielsweise auf einen Standardwert gesetzt werden, oder in Abhängigkeit der Eingabeparameter initialisiert werden.

Wir können anschließend im Hauptprogramm sogenannte Instanzen unserer Klasse erzeugen mit

<instance> = <class_name>(<param1>, <param2>, ...)

Auf die Attribute der Klasse können wir innerhalb der Klasse (also in der Implementierung einer Methode) mit

self.<variable>

und von außerhalb der Klasse mit

<instance>.<variable>

zugreifen und diese sowohl lesen als auch schreiben. Ähnlich verhält es sich bei dem Aufruf von Methoden. Innerhalb der Klasse nutzen wir

self.<method>(<param1>, <param2>, ...)

und außerhalb

<instance>.<method>(<param1>, <param2>, ...)

Für die Verwaltung von Studierenden wäre für eine objektorientierte Lösung zunächst die folgende Definition einer Klasse sinnvoll:

class student:
    
    # Constructor
    def __init__(self, name, sex, age, course="nicht eingeschrieben", semester=1):
        
        print(name, "immatrikuliert")
        self.name     = name
        self.sex      = sex
        self.age      = age
        self.course   = course
        self.semester = semester
        self.marks    = []                
        
    # Increases and returns age of a student
    def celebrate_birthday(self):
        print(self.name, "feiert Geburtstag")
        self.age += 1 
        return self.age

Dieser Code erfordert eine ausführlichere Erläuterung:

  • Wir haben eine Klasse namens student definiert.

  • Die Klasse besitzt die Attribute name, sex, age, course, semester, marks, welche in der Init-Funktion zunächst initialisiert werden. Die Init-Funktion initialisiert die Attribute name, sex, age anhand der Eingabeparameter mit der wir die Init-Funktion später aufrufen werden. Für die Attribute course und semester wurden Default-Werte angegeben (beachte die Schreibweise course="nicht eingeschrieben" und semester=1 in der Parameterliste der Init-Funktion). Diese Default-Werte werden verwendet, wenn die Parameter beim Aufruf dieser Methode weggelassen werden. Das Attribut marks wurde einfach mit einem sinnvollen Default-Wert initialisiert - einer leeren Liste - und ist unabhängig von den Eingabeparametern.

  • Die Klasse besitzt eine Methode namens celebrate_birthday. Diese Methode erhöhrt das Attribut age um 1 und gibt das neue Alter zurück.

Beachte: Bei allen Methoden einer Klasse ist der erste Funktionsparameter immer self, also der Zeiger auf die Instanz selbst. Beim Aufruf einer Funktion wird dieses Objekt nicht mit übergeben.

Schauen wir uns nun an, wie wir mit unserer Klasse arbeiten können. Wir erzeugen zunächst 2 Instanzen dieser Klasse:

# Instanzen der Klasse student erzeugen (ruft Init-Funktion auf)
lisa = student("Lisa", "w", 23, "Mathematik", 8)
bernd = student("Bernd", "m", 28, "Maschinenbau")
Lisa immatrikuliert
Bernd immatrikuliert

An der Konsolenausgabe erkennen wir, dass tatsächlich die Init-Funktion aufgerufen wurde. Somit sind auch die Attribute sinnvoll initialisiert. Wir können beispielsweise Lisa’s Alter abfragen, also auf ein Attribut zugreifen:

# Zugriff auf Klassenvariable
print("Lisa ist", lisa.age, "Jahre alt")
Lisa ist 23 Jahre alt

Um eine Methode auf Lisa aufzurufen nutzen wir

# Aufrufen einer Methode
new_age = lisa.celebrate_birthday()
print("Lisa ist jetzt", new_age, "Jahre alt")
Lisa feiert Geburtstag
Lisa ist jetzt 24 Jahre alt

Dies waren schon die wichtigsten Konzepte zu Klassen. Wir sollten an diesem Punkt Attribute und Methoden, sowie den Sinn der Init-Funktion verstanden haben.

Übungsaufgabe

Schreibe eine Klasse book, welche ein Lehrbuch repräsentieren soll. Die Klasse soll in der Init-Funktion Attribute wie Author, Titel, Regalstandort und Ausleihstatus anhand entsprechender Eingabeparameter initialisieren. Standardmäßig ist ein Buch nicht ausgeliehen. Implementiere außerdem eine Methode rent, welche im Fall einer Ausleihe aufgerufen wird und die entsprechenden Attribute modifiert, sowie eine Methode is_rent, welche abfragt, ob ein Buch verliehen ist. In diesem Fall soll True zurückgegeben werden, andernfalls False.

Einige weitere Begriffe müssen wir noch einführen. Das, was wir bisher als Attribut bezeichnet haben, sollte eigentlich den Begriff Instanzattribut tragen, denn es ist an eine bestimmte Instanz unserer Klasse gekoppelt. Eine weitere Art von Attribut ist das sogenannte Klassenattribut (auch Klassenvariable). Ein Klassenattribut ist eine Variable, die sich alle Instanzen einer Klasse teilen. Ein Klassenaatribut wird im Rumpf einer Klasse definiert

class <class_name>:
    <class_attribute> = <value>

und sowohl innerhalb als auch außerhalb der Klasse mit

<value> = <class_name>.<class_attribute>
<class_name>.<class_attribute> = <value>

gelesen bzw. geschrieben. Wir können beispielsweise unsere student-Klasse um ein Klassenattribut namens count erweitern, mit dem wir später die Anzahl der immatrikulierten Studierenden zählen können. Der Counter soll in der Init-Funktion erhöhrt und im Gegenstück, der Delete-Funktion oder auch Destruktor (diese wird aufgerufen, wenn ein Objekt zerstört wird) verringert wird. Wir könnten im Jupyter-Notebook die Zelle, in der wir die Klasse student definiert haben, editieren, oder die Klasse durch die rekursive Klassendefinition class student(student) um eine Methode erweitern, bzw. eine bestehende Methode ersetzen:

class student(student):
    # Klassenattribut definieren
    count = 0
    
    # Init-Funktion
    def __init__(self, name, sex, age, course="nicht eingeschrieben", semester=1):
        
        print(name, "immatrikuliert")
        self.name     = name
        self.sex      = sex
        self.age      = age
        self.course   = course
        self.semester = semester
        self.marks    = [] 
        
        # Klassenattribut schreiben
        student.count += 1 

    def __del__(self):
        print(self.name, "exmatrikuliert")

        # Klassenattribut schreiben
        student.count -= 1

Wir erzeugen wieder 2 Instanzen der Klasse student, lesen das Klassenattribut count, löschen einen Studenten, und lesen das Klassenattribut erneut:

lisa = student("Lisa", "w", 23, "Mathematik", 8)
bernd = student("Bernd", "m", 28, "Maschinenbau")

print("Anzahl Studierende:", student.count)

del bernd # Dies ruft die Delete-Funktion auf
print("Anzahl Studierende:", student.count)
Lisa immatrikuliert
Bernd immatrikuliert
Anzahl Studierende: 2
Bernd exmatrikuliert
Anzahl Studierende: 1

Wir fassen zunächst die gelernten Begriffe zusammen:

Bezeichnung

Beispiel

Klasse

student

Instanz

lisa, bernd

Methode

student.celebrate_birthday()

Instanzattribut

self.name, self.age, …

Klassenattribut

student.count

Mit der Syntax

<instanz>.<locale variable>

können wir auf die Instanzvariablen zugreifen, und mit

<instanz>.<function>(<param1>, <param2>, ...)

Methoden aufrufen.

Wir haben bereits spezielle Methoden kennengelernt, die nicht explizit aufgerufen werden, sondern eine Sonderrolle einnehmen:

Name

Bezeichnung

Bemerkung

__init__(self, <param_list>)

Konstruktor

Beim Erstellen einer Instanz aufgerufen

__del__(self)

Destruktor

Beim Löschen einer Instanz mit del <instance> aufgerufen

Fügen wir nun noch einige Funktionen zu unserer Klasse hinzu:

class student(student):
    # Füge Prüfungsergebnis hinzu
    def add_exam(self, mark):
        self.marks.append(mark)
        
    # Berechne Notendurchschnitt
    def get_average_mark(self):
        nr_marks = len(self.marks)
        if nr_marks > 0:
            return sum(self.marks) / len(self.marks)
        else:
            return 0

Da wir die Klasse student verändert haben, müssen wir die Instanz lisa nochmal neu anlegen. Für diese Instanz können wir nun Prüfungsnoten hinzufügen und den Notendurchschnitt berechnen lassen:

lisa = student("Lisa", "w", 23, "Mathematik", 8)
lisa.add_exam(2.0)
lisa.add_exam(1.3)
avg = lisa.get_average_mark()
print(lisa.name, "hat einen Notendurchschnitt von", avg)
Lisa immatrikuliert
Lisa exmatrikuliert
Lisa hat einen Notendurchschnitt von 1.65

Interessant ist hier die Konsolenausgabe. Es wird offensichtlich Init- und Delete-Funktion von student aufgerufen. Das liegt daran, dass der name lisa bereits an ein Objekt vom Typ student gebunden ist. Beim Aufruf von lisa = student(...) wird ein neues Objekt erstellt und der Name lisa wird an dieses gebunden. Das ursprüngliche Lisa-Objekt wird nun nicht mehr referenziert und wird von Python daher gelöscht, was einen Aufruf der Delete-Funktion zur Folge hat.

5.2.1. Spezielle Methoden#

Schauen wir uns nochmal die Init-Funktion __init__ an. Diese wird nicht explizit aufgerufen, sondern bei der Initialisierung einer Klasseninstanz über student(...). Es gibt noch weitere solcher Funktionen, die, falls sie in der Klasse implementiert sind, die weitere Arbeit mit der Klasse eleganter gestaltet.

Probieren wir nun mal die Instanz lisa auf der Konsole auszugeben:

print("Das ist Lisa:", lisa)
Das ist Lisa: <__main__.student object at 0x7f32646d7d00>
lisa
<__main__.student at 0x7f32646d7d00>

Standardmäßig wird ein Text ausgegeben, der uns verrät zu welcher Klasse Lisa gehört, und an welcher Stelle des Speichers Lisa liegt. Wünschenswert wäre vielleicht eine schön formattierte Konsolenausgabe.

Dazu müssen wir die Klasse um 2 weitere versteckte Funktionen erweitern:

class student(student):
    
    # Aufgabe einer Instanz als String
    def __to_string__(self):
        return self.name + ", "  \
    + ("männlich" if self.sex == "m" else "weiblich") + ", " \
    + str(self.age) + " Jahre, " \
    + self.course + " (" + str(self.semester) + ". Semester)"
    
    # Konvertierung zu String bei print
    def __str__(self):
        return self.__to_string__()
    
    # Konvertierung zu String bei Standard-Konsolenausgabe
    def __repr__(self):
        return self.__to_string__()

Die Methode __str__ wird bei der Konsolenausgabe mit print aufgerufen, und die Methode __repr__, falls wir einfach nur lisa in die Konsole eintippen. Da beide Funktionen hier das Gleiche ausgeben sollen, haben wir die eigentliche Umwandlung in einen String in die Funktion __to_string__ ausgelagert.

lisa = student("Lisa", "w", 23, "Mathematik", 8)
print("Das ist Lisa:", lisa)
Lisa immatrikuliert
Das ist Lisa: Lisa, weiblich, 23 Jahre, Mathematik (8. Semester)
lisa
Lisa, weiblich, 23 Jahre, Mathematik (8. Semester)

Wir haben bei der Methode __to_string__ auch doppelte Unterstriche vorangestellt. Dies bedeutet im Allgemeinen, dass die Methode privat ist und lediglich von anderen Mehoden der Klasse, aber nicht von außen aufgerufen werden soll. Prinzipiell ist der Aufruf lisa.__to_string()__ zwar erlaubt, sollte aber von der Programmiererin nicht verwendet werden.

Ferner sind Vergleichsoperationen interessant. Wir wollen vielleicht über Vergleiche mit < Personen nach Namen sortieren, oder mit == doppelt angelegte Instanzen ermitteln.

Solche Vergleichsoperationen müssen natürlich irgendwo in unserer Klasse definiert sein. Dazu implementiert man die Funktion __lt__(self,other) (“lt” steht für “less than”), und für die Operationen <= noch __le__ (less or equal) und für == noch __eq__(self,other) (equal):

class student(student):
    
    # Comparison operation <
    def __lt__(self, other):
        return self.name < other.name    
    
    # Comparison operation <=
    def __le__(self, other):
        return self.name <= other.name
    
    # Comparison operation ==
    def __eq__(self, other):
        return self.name == other.name
lisa  = student("Lisa", "w", 23, "Mathematik", 8)
bernd = student("Bernd","m", 25, "Maschinenbau", 6)
lisa2 = student("Lisa", "w", 21, "Psychologie", 2)

print("Lisa kleiner Bernd       : ", lisa < bernd)
print("Lisa kleiner/gleich Lisa : ", lisa <=bernd)
print("Lisa gleich Lisa2        : ", lisa == lisa2)
print("Lisa größer Bernd        : ", lisa > bernd)
print("Lisa größer/gleich Bernd : ", lisa >= bernd)
Lisa immatrikuliert
Bernd immatrikuliert
Lisa immatrikuliert
Lisa kleiner Bernd       :  False
Lisa kleiner/gleich Lisa :  False
Lisa gleich Lisa2        :  True
Lisa größer Bernd        :  True
Lisa größer/gleich Bernd :  True

Mit der Implementierung von __le__ bzw __lt__ können wir also die Operatoren > bzw. >= verwenden. Da wir nun eine Vergleichsoperation haben, ist auch der sort-Befehl in Listen von Instanzen der Klasse student ausführbar:

student_list = [student("Lisa", "w", 23, "Mathematik", 8),
                student("Bernd", "m", 25, "Maschinenbau", 6),
                student("Tom", "m", 32, "Psychologie", 22),
                student("Anna", "w", 19, "Chemie", 2)]

student_list.sort()
student_list
Lisa immatrikuliert
Bernd immatrikuliert
Tom immatrikuliert
Anna immatrikuliert
[Anna, weiblich, 19 Jahre, Chemie (2. Semester),
 Bernd, männlich, 25 Jahre, Maschinenbau (6. Semester),
 Lisa, weiblich, 23 Jahre, Mathematik (8. Semester),
 Tom, männlich, 32 Jahre, Psychologie (22. Semester)]

Auch unsere Implementierung der String-Darstellung aus __str__ wurde bei der Konsolen-Ausgabe verwendet.

5.2.2. Überladung von Operatoren#

Auch die üblichen Rechenoperationen lassen sich für eigene Klassen definieren. Wie schon im Kapitel {ref}numpy gesehen, war die Addition, Subtraktion, Multiplikation und Division für Instanzen vom Typ numpy.ndarray definiert. Um dies für eigene Klassen zu realisieren müssen wir beispielsweise für die +-Operation die Methode __add__(self, other) implementieren. Da die Addition zweier Studierender wenig Sinn macht, wechseln wir hier das Beispiel und implementieren eine eigene Klasse für komplexe Zahlen:

class my_complex:

    # Init-Funktion
    def __init__(self, real, imag=0.):
        self.real = real
        self.imag = imag

    # Konsolenausgabe
    def __to_string__(self):
        return str(self.real) + ("+" if self.imag >= 0 else "") + str(self.imag) + "i"
    def __str__(self):
        return self.__to_string__()
    def __repr__(self):
        return self.__to_string__()

    # Addition
    def __add__(self, other):
        return my_complex(self.real+other.real, self.imag+other.imag)

In folgendem Beispiel werden 2 komplexe Zahlen angelegt, addiert und das Ergebnis wird auf die Konsole geschrieben:

x = my_complex(1., 3.)
y = my_complex(1., -2.)

z = x + y # ruft __add__ auf
print(x, "+", y, "=", z)
1.0+3.0i + 1.0-2.0i = 2.0+1.0i

Übungsaufgabe

In der Funktion my_complex.__to_string__ haben wir erstmals eine implizite if-Abfrage verwendet. Versuche selbst nachzuvollziehen wie diese funktioniert. Schreibe alternativ eine herkömmliche if-Abfrage für die Ausgabe des richtigen Vorzeichens vor dem Imaginärteil.

In der folgenden Tabelle sind weitere vordefinierte Methoden zur Operatorüberladung zusammengefasst:

Operator

Methode

+

__add__(self,other)

-

__sub__(self,other)

*

__mul__(self,other)

/

__div__(self,other)

**

__pow__(self,other)

==

__eq__(self, other)

>=

__ge__(self, other)

>

__gt__(self, other)

<=

__le__(self, other)

<

__lt__(self, other)

Übungsaufgabe

Vervollständige die Klasse my_complex und implementiere, ausgenommen der Vergleichsoperatoren, alle Operatoren aus der Tabelle. Teste die Implementierung an einfachen Beispielen.

5.3. Vererbung#

Bei der Vererbung übernimmt eine Klasse die Eigenschaften und Methoden einer anderen Klasse und ergänzt diese gegebenenfalls durch weitere Eigenschaften und Methoden. Dies ist sinnvoll, wenn man mit mehreren Klassen arbeiten möchte, für die viele Eigenschaften gleich sind. Um eine Klasse von einer anderen abzuleiten nutzen wir die Syntax

class <sub_class>(<base_class>):
    [...]

Als Beispiel betrachten wir

class animal:
    # Klassenattribute
    description = "unknown animal"
    region = "somewhere"
            
    # Konsolenausgabe für alle Tiere
    def __str__(self):
        return "I am a "+self.description+" and I live in/at "+self.region+".";
    
class fish(animal):
    # Klassenattribute
    description = "fish"
    region = "water";
    
    # Konstruktor für Fische
    def __init__(self, color):        
        # Instanzattribute
        self.color = color
        
class mammal(animal):
    # Klassenattribute
    description = "mammal"
    region = "land"
    
    def __init__(self, nr_legs):
        # Instanzattribute
        self.nr_legs = nr_legs

# Hauptprogramm beginnt hier
carp = fish("blue")
print(carp)

monkey = mammal(2)
print(monkey)
I am a fish and I live in/at water.
I am a mammal and I live in/at land.

Die Klassenattribute description und region sind sowohl in der Basisklasse, als auch in der abgeleiteten Klasse definiert. Diese nehmen aber stets den Wert der abgeleiteten Klasse an. Die Funktion für die Konsolenausgabe __str__ mussten wir damit nur in der Basisklasse definieren.

Wenn wir nun die __str__-Funktion in der abgeleiteten Klasse mammal erneut implementieren, dann wird auch diese für alle Objekte vom Typ mammal aufgerufen und nicht die Implementierung der Basisklasse. Wir können aber mit animal.__str__ weiterhin auf die Implementierung dieser Funktion aus der Basisklasse zugreifen:

class mammal(animal):
    
    description = "mammal"
    region = "land"
    
    def __init__(self, nr_legs):
        self.nr_legs = nr_legs
        
    def __str__(self):
        return animal.__str__(self)+" I have "+str(self.nr_legs)+" legs."
    
human = mammal(2)
print(human)
I am a mammal and I live in/at land. I have 2 legs.

Python erlaubt natürlich auch weitere Ebenen der Vererbung. Wir könnten von unserer Klasse für Säugetiere noch weitere Klassen ableiten, für Nagetiere, Huftiere, etc.. Auch eine Mehrfachvererbung ist möglich. Möchten wir eine Klasse von 2 anderen ableiten, dann schreiben wir einfach

class <sub_class>(<base_class_1>, <base_class_2>[, ...]):
    [...]

und unsere neue Klasse erbt alle Eigenschaften und Methoden von <base_class_1> und <base_class_2>. Kritisch wird es nur dann, wenn beide Klassen Methoden mit gleichem Namen aber unterschiedlicher Implementierung bereitstellen:

class horse:
    def __init__():
        print("Horse constructor called")
    def output():
        print("Hüüüü")
class human:
    def __init__():
        print("Human constructor called")
    def output():
        print("Arghh")

# Klasse für ein Mischwesen
class centaur(horse, human):
    pass

a = centaur
a.output()
Hüüüü

5.4. Iteratoren#

Wir haben schon einige Datentypen gesehen, über die wir in einer for-Schleife iterieren können, beispielsweise list, tuple, string. Man kann auch selbst programmierte Klassen iterierbar machen. Wichtig ist hierfür die Implementierung der folgenden 2 Funktionen:

  • __iter__(self): Initialisiert den Iterator und gibt gibt das aktuelle Objekt selbst zurück.

  • __next__(self): Gibt das Nachfolgeelement der Iteration zurück oder wirft die Exception StopIteration

Als Beispiel implementieren wir eine Klasse zur Ziehung der Lottozahlen:

import random

class LotteryNumbers:
    
    def __iter__(self):
        self.n = 0                 # Initialisiere Zähler
        return self                # Rückgabe des aktuellen Objekts
    def __next__(self):
        self.n += 1                # Inkrementiere Zähler
        if self.n >=7:
            raise StopIteration    # Abbruch nach 6 Zahlen
        else:
            return random.randint(1,49) # Erzeuge Zufallszahl

Diese 2 Funktionen reichen aus um mit einer for-Schleife über dieses Objekt zu iterieren:

for i in LotteryNumbers():
    print("Die Zahl", i, "wurde gezogen.")
Die Zahl 39 wurde gezogen.
Die Zahl 40 wurde gezogen.
Die Zahl 6 wurde gezogen.
Die Zahl 23 wurde gezogen.
Die Zahl 12 wurde gezogen.
Die Zahl 43 wurde gezogen.

Ein iterierbares Objekt lässt sich auch wie folgt manuell durchgehen:

numbers = iter(LotteryNumbers()) # Erzeuge Iterator
try:
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
    print("Lottozahl", next(numbers))
except:
    print("Alle Zahlen wurden gezogen")
Lottozahl 26
Lottozahl 44
Lottozahl 47
Lottozahl 1
Lottozahl 26
Lottozahl 29
Alle Zahlen wurden gezogen

Das Konstrukt bestehend aus try und except dient zum “Exception-Handling”. Python versucht den Block unter try auszuführen und springt, sobald eine dieser Funktionen eine Exception wirft (beachte die Zeile raise StopIteration in LotteryNumbers.__next(self)__), zum except-Block.

5.5. Code-Dokumentation#

Damit es potentielle Anwender unserer selbst programmierten Klassen einfacher haben, ist eine ordentliche Code-Dokumentation sehr hilfreich. Wir können die Klasse selbst, sowie alle Methoden mit Kommentaren der Form

"""
[Comment]
...
[Comment]
"""

versehen. Dieser Text erscheint dann im Hilfetext im Jupyter-Notebook, wenn wir mit ? nach der Dokumentation einer Klasse oder Methode fragen. Hier ein Beispiel einer gut dokumentierten Klasse:

class Car:
    """
    A car object, equipped with a model name and color.
    """
    
    def __init__(self, model, color="gray"):
        """
        Constructor
        
        Initializes a new car object with a default name. The engine is off.
        
        Parameters
        ----------
        model: string
            Model of the car (e.g. VW, Mercedes, ...)
        color: string, optional
            Color of the car (e.g. red, blue, ...). Default value: gray
        """
        self.model = model
        self.color = color
        self.engine_on = False
        
    def start_engine(self):
        """
        start_engine(self)
        
        Method that allows to start the engine
        
        Example
        -------
        >>> mercedes = Car()
        >>> mercedes.start_engine()
        """
        
        self.engine_on = True
    
    def engine_status(self):
        """
        engine_status()
        
        Returns the engine status.
        
        Returns
        -------
        out: bool
            Returns True when the engine is on or False when it is off
            
        See Also
        --------
        start_engine : Method used to start the engine
        """
        
        return engine_on

Wir bekommen nun mit

Car?

unsere Klassendokumentation angezeigt und mit

Car.engine_status?

die Dokumentation der engine_status-Methode.