Klassen

objektorientierte Programmierung

Veranschaulichung von Klassen: Obst als Klasse Auch wenn Python ohne Wenn und Aber eine objektorientierte Programmiersprache ist, sind wir in den vorhergehenden Kapiteln nur indirekt auf die objektorientierte Programmierung (OOP) eingegangen. Mit Python lassen sich kleine Skripte oder Programme einfach und effizient schreiben, auch ohne dass man sie objektorientiert modelliert. Gerade totale Programmieranfänger finden es erfahrungsgemäß einfacher, wenn sie nicht sofort mit allen Prinzipien der OOP konfrontiert werden. Sie haben genügend Probleme, Zuweisungen, bedingte Anweisung oder Schleifen zu verstehen und vor allem richtig anzuwenden. Aber in vielen Situationen stellt die OOP eine deutliche qualitative Verbesserung der Implementierung eines Problems dar. Aber auch wenn wir die objektorientierte Programmierung in den bisherigen Kapiteln vermieden haben, so war sie dennoch in unseren Übungen und Beispielen meistens präsent. Wir haben Objekte und Methoden von Klassen benutzt, ohne eigentlich von ihrer Existenz zu wissen. In diesem Kapitel geben wir nun eine grundlegende Einführung in den objektorientierten Ansatz von Python. OOP ist eine der mächtigsten Programmiermöglichkeiten von Python, aber, wie wir gesehen haben, muss man sie dennoch nicht nutzen, d.h. man kann auch umfangreiche und effiziente Programme ohne Verwendung von OOP-Techniken schreiben.

Auch wenn viele Programmierer und Informatiker die OOP für eine moderne Errungenschaft halten, so gehen ihre Wurzeln bis in die 1960er-Jahre zurück. Die erste Programmiersprache, die Objekte verwendete, war ,,Simula 67'' von Ole-Johan Dahl und Kristen Nygard. Wie der Name sagt wurde diese Sprache bereits im Jahre 1967 eingeführt.

Objekte, Instanzen und Klassen

Ein Grundkonzept der objektorientierten Programmierung besteht darin, Daten und deren Funktionen (Methoden), - d.h. Funktionen, die auf diese Daten angewendet werden können -, in einem Objekt zusammenzufassen und nach außen zu kapseln, sodass die Benutzer der Klassen und auch Methoden fremder Objekte diese Daten nicht manipulieren können.

Objekte werden über Klassen definiert. Klassen sind Vorlagen - man könnte auch ,,Baupläne'' sagen -, nach denen Objekte, - die man in diesem Zusammenhang auch als Instanzen bezeichnet - zur Laufzeit des Programmes erzeugt werden. Eine Klasse stellt eine formale Beschreibung dar, wie ein Objekt beschaffen ist, d.h. welche Attribute und welche Methoden sie hat. Eine Klasse darf nicht mit einem Objekt verwechselt werden. Statt von einem Objekt spricht man auch von einer Instanz einer Klasse, d.h. die Begriffe ,,Instanz'' und ,,Objekt'' werden meistens synonym benutzt.
Obst als Klasse

Man kann eine Klasse auch im übertragenen Sinne wie ein Koch- oder Backrezept sehen. Betrachten wir beispielsweise das Rezept eines Erdbeerkuchens. Ein solches Rezept kann man prinzipiell als eine Klasse ansehen. Das heißt, das Rezept bestimmt, wie eine Instanz der Klasse beschaffen sein muss. Backt jemand einen Kuchen nach diesem Rezept, dann schafft er eine Instanz oder ein Objekt dieser Klasse. Es gibt dann verschiedene Methoden, diesen Kuchen zu verarbeiten oder zu verändern, wie zum Beispiel ,,Teig anrühren''. Ein Erdbeerkuchen gehört in eine übergeordnete Klasse ,,Kuchen'', die ihre Eigenschaften, z.B. dass ein Kuchen sich als Nachtisch nutzen lässt, an Unterklassen wie Erdbeerkuchen, Rührkuchen, Torten und so weiter vererbt.


Ein Objekt bezeichnet in der OOP die Abbildung eines realen Gegenstandes mit seinen Eigenschaften und Verhaltensweisen (Methoden) in ein Programm.

Ein Objekt kann immer durch zwei Dinge beschrieben werden:

  • was es tun kann oder was wir in einem Programm mit ihm tun können,
  • was wir über es wissen.

Eigenschaften und Methoden

Im nebenstehenden Diagramm sehen wir die Modellierung einer Klasse Konto mit ihren Eigenschaften und Methoden. Die Eigenschaften der Klasse Konto müssen auf jeden Fall eine "Kontonummer" und einen "Kontostand" haben. Dazu braucht man auch Eigenschaften wie "Verfügungsberechtigte" und "Kreditrahmen". Natürlich braucht ein Konto auch einen Inhaber. Das Attribut bzw. die Eigenschaft für den Inhaber ist eine Referenz auf eine andere Klasse, nämlich die Kontoinhaber-Klasse. Verändert werden die Attribute über die Methoden. So kann man beispielsweise den "Kontostand" nur mittels der Methoden "Einzahlen" und "Auszahlen" verändern.

Eine Klasse ist ein abstrakter Oberbegriff für die Beschreibung der gemeinsamen Struktur und des gemeinsamen Verhaltens von realen Objekten (Klassifizierung). Reale Objekte werden auf die für die Software wichtigen Merkmale abstrahiert. Die Klasse dient als Bauplan zur Abbildung von realen Objekten in Software-Objekte, die sogenannten Instanzen. Die Klasse fasst hierfür notwendige Eigenschaften (Attribute) und zur Manipulation der Eigenschaften notwendige Methoden zusammen. Klassen stehen häufig in Beziehung zueinander. Man hat beispielsweise eine Oberklasse (Kuchen), und aus dieser leitet sich eine andere Klasse ab (Erdbeerkuchen). Diese abgeleitete Klasse erbt bestimmte Eigenschaften und Methoden der Oberklasse.

Kapselung von Daten

Ein weiterer wesentlicher Vorteil der OOP besteht in der Kapselung von Daten. Der Zugriff auf Eigenschaften darf nur über Zugriffsmethoden erfolgen. Diese Methoden können Plausibilitätstests, Datentypwandlungen oder beliebige Berechnungen enthalten, und sie (oder ,,nur'' sie) besitzen ,,Informationen'' über die eigentliche Implementierung.

Roboter und Konto

Im nächsten Abschnitt werden wir eine Roboterklasse in Python schreiben. Diese wird beispielsweise Informationen über das Baujahr und den Namen eines Roboters enthalten. Diese Informationen bezeichnet man auch als Eigenschaften bzw. Attribute einer Instanz. Es bietet sich beispielsweise an, den Namen eines Roboters als String in einem Attribut zu speichern. Datenkapselung bedeutet nun, dass wir nicht direkt auf diesen String zugreifen können. Wir müssen beispielsweise eine Methode ,,HoleNamen()'' aufrufen, um den Namen eines Roboters zu erhalten.

Das Prinzip der Datenkapselung kann man auch schön am Modell der Kontenklasse sehen. Die Methode zum Setzen des Geburtsdatums kann beispielsweise prüfen, ob das Datum korrekt ist: So kann man abfangen, wenn jemand wegen eines Tippfehlers ein Datum in der Zukunft angibt. Man könnte auch generell prüfen ob sich die Angabe innerhalb eines bestimmten Rahmens bewegt. So soll beispielsweise ein Girokonto für Kinder unter 14 nicht möglich sein oder Anlagen von Neukunden über 100 Jahre gelten als extrem unwahrscheinlich. ''

Vererbung

Ausgehend von einer allgemeinen Roboterklasse, die lediglich einen Namen und ein Baujahr kennt, könnten wir uns weitere Roboterklassen definieren wie beispielsweise "Industrieroboter", die stationär an einem Fließband eingesetzt werden können, bewegliche Roboter mit Rädern, Beinen oder Raupen und so weiter. Jede dieser Klassen erbt dann von der Basisklasse die Möglichkeit einen Namen und ein Baujahr zu haben. Wie dies genau abläuft, werden wir im folgenden Unterkapitel kennen lernen.

Bei der Konto-Klasse kann man Klassen wie Sparkonto und Girokonto definieren:

Vererbung in der Kontoklasse



Klassen in Python

Objekte und Instanzen einer Klasse

Eine der vielen in Python integrierten Klassen ist die list-Klasse, die wir bereits häufig in unseren Übungen und Beispielen benutzt hatten. Die list-Klasse stellt eine Fülle von Methoden zur Verfügung, mit deren Hilfe wir zum Beispiel Listen aufbauen, Elemente anschauen, verändern und entfernen können:

x = [3, 6, 9]
y = [45, "abc"]
print(x[1])
x[1] = 99
x.append(42)
last = y.pop()
print(last)
6
abc

Die Variablen x und y bezeichnen zwei Instanzen der list-Klasse. Vereinfacht haben wir bisher gesagt ,,x und y sind Listen''. Im Folgenden werden wir die Begriffe ,,Objekt'' und ,,Instanz'' synonym benutzen, wie dies auch in anderen Einführungen üblich ist.1

Kapselung von Daten und Methoden

pop und append aus dem obigen Beispiel sind Methoden der list-Klasse. pop liefert uns das ,,oberste'' bzw. das Element mit dem höchsten Index der Liste zurück und entfernt dieses Element. Wir wissen allerdings nicht, wie die Listen intern im Speicher abgelegt sind. Wir brauchen diese Information auch nicht, da uns die list-Klasse Methoden zur Verfügung stellt, auf die gespeicherten Daten ,,indirekt'' zuzugreifen. Methoden sind von besonderer Wichtigkeit im Zusammenhang mit der Datenkapselung. Wir werden uns später genauer mit der Datenkapselung beschäftigen. Sie verhindert den direkten Zugriff auf die interne Datenstruktur, d.h. man kann nur über definierte Schnittstellen zugreifen, d.h. die Methoden. Die ,,internen Daten'' einer Instanz sind die Attribute, auf die wir später zu sprechen kommen.

Ein minimale Klasse in Python

Evolution der Roboter

Die wichtigsten Begriffe der objektorientierten Programmierung und Ihrer Umsetzung in Python werden wir im Folgenden an einem Beispiel ,,Roboterklasse'' demonstrieren. Wir beginnen mit einer einfachen Klasse in Python, die wir "Roboter" nennen.

class Roboter:
    pass

An diesem Beispiel können wir den grundlegenden syntaktischen Aufbau einer Klasse erkennen: Eine Klasse besteht aus zwei Teilen: dem Kopf und dem Körper. Der Kopf besteht meist nur aus einer Zeile: das Schlüsselwort class, gefolgt von einem Leerzeichen, einem beliebigen Namen, - in unserem Fall Roboter - einer kommaseparierten Auflistung von Oberklassen in Klammern und als letztes Zeichen ein Doppelpunkt. Gibt es keine Oberklassen, entfällt die Angabe der Oberklassen und der Klammern. Prinzipiell kann auch ein leeres Klammernpaar vor dem Doppelpunkt stehen. Das Klammernpaar mit der Auflistung der Oberklassen brauchen Sie zum jetzigen Zeitpunkt noch nicht zu verstehen, da wir erst später darauf eingehen!

Der Körper einer Klasse besteht aus einer eingerückten Folge von Anweisungen die wie in unserem Beispiel auch nur aus einer einzigen pass-Anweisung bestehen kann.

Damit haben wir bereits eine einfache Python-Klasse mit dem Namen Roboter definiert. Wir können diese Klasse auch benutzen:



class Roboter:
    pass
x = Roboter()
y = Roboter()
y2 = y
print(y == y2)
print(y == x)
True
False

Wir haben in obigem Beispiel zwei verschiedene Roboter x und y geschaffen. Außerdem haben wir mit y2 = y ein Alias y2 für y erzeugt. Dabei handelt es sich nur um einen weiteren Namen für das gleiche Objekt, also um eine Referenz.

Eigenschaften und Attribute

Unsere Roboter haben keinerlei Eigenschaften. Noch nicht einmal einen Namen, wie dies für ,,ordentliche'' Roboter üblich ist. Als weitere Eigenschaften wären beispielsweise eine Typbezeichnung, Baujahr und so weiter denkbar. Eigenschaften werden in der objektorientierten Programmierung als Attribute bezeichnet. 2



Einer Instanz kann man beliebige Attributnamen zuordnen. Sie werden mit einem Punkt an den Namen der Instanz angeschlossen. Im Folgenden erzeugen wir dynamisch Attribute für den Roboternamen und das Roboterbaujahr. Bitte beachten Sie, dass dies noch nichts mit den eigentlichen Attributen zu tun hat, wie wir sie in Klassen verwenden werden:

class Roboter:
    pass
x = Roboter()
y = Roboter()
 
x.name = "Marvin"
x.baujahr = 1979
y.name = "Caliban"
y.baujahr = 1993
print(x.name)
Marvin

Attribute können übrigens auch dem Klassenobjekt selbst oder -- unabhängig von der OOP -- Funktionen zugeordnet werden, wie wir im Folgenden sehen:

class Roboter:
    pass
Roboter.number = 1000
print(Roboter.number)
def f(x):
    return 42
f.color = "red"  # was immer es bedeuten soll, einer Funktion ein Farbattribut zuzuordnen :-)
print(f.color)
# unsere Funktion ist davon aber nicht betroffen:
print(f(10))
1000
red
42

Attribute bei Funktionen können zum Beispiel als Ersatz für statische Funktionsvariablen, wie manche Sie von C, C++ oder Java her kennen, benutzt werden. Python kennt keine statischen Funktionsvariablen!

In der folgenden Funktion wird das Attribut "zaehler" benutzt, um zu zählen, wie oft die Funktion aufgerufen wird:

def f(x):
    if hasattr(f, "counter"): # Alternativ: if "counter" in dir(f):
        f.counter += 1
    else:
        f.counter = 0
    return x + 3
        
for i in range(10):
    f(i)
    
print(f.counter)
9

Noch ein Detail, was zum jetzigen Zeitpunkt noch nicht so wichtig ist. Sie können also gerne mit dem nächsten Unterabschnitt "Methoden" weiter machen.

Die Objekte der meisten Klassen haben ein Attributdictionary dict, in dem die Attribute mit ihren Werten gespeichert werden, wie wir es im nächsten Beispiel sehen.

class Robot:
    pass
x = Robot()
x.name = "Marvin"
x.age = 5
x.__dict__
Ausgabe: :

{'name': 'Marvin', 'age': 5}



Die dynamische Erzeugung von Attributen für Instanzen sehen manche als Segen und andere als Fluch an. Aber Attribute, wie man sie in der objektorientierten Programmierung verwendet, werden so nicht erzeugt.
Wenn wir Instanzattribute für unsere Roboterklasse erzeugen wollen, so müssen wir dies unmittelbar in der Klassendefinition tun. Instanzattribute sind die Eigenschaften, die die einzelnen Instanzen beschreiben, d.h. so haben unsere Roboter im allgemeinen verschiedene Namen, und sicherlich eine verschiedene Seriennummer. Wir benötigen Methoden, um Instanzattribute in einer Klassendefinition zu erzeugen.



Methoden


Hi, I'm Marvin

Im Folgenden wollen wir zeigen, wie man Methoden in einer Klasse definiert. Dazu werden wir unsere leere Roboterklasse um eine Methode SageHallo erweitern.

Eine Methode unterscheidet sich äußerlich nur in zwei Aspekten von einer Funktion:

  • Sie ist eine Funktion, die innerhalb einer class-Definition definiert ist.
  • Der erste Parameter einer Methode ist immer eine Referenz auf die Instanz, von der sie aufgerufen wird. Diese Referenz wird üblicherweise mit ,,self'' genannt.

Anmerkung: Prinzipiell könnte man einen beliebigen Namen statt ,,self'' wählen, also auch ,,this'', was Java oder C++-Programmierern vielleicht besser gefallen würde. ,,self'' ist nur eine Konvention.



Wir erweitern nun unsere Roboterklasse um eine Methode SageHallo, die beim Aufruf einfach nur ,,Hallo'' schreibt:

class Roboter:
    def SageHallo(self):
        print("Hallo")
x = Roboter()
x.SageHallo()
Hallo

Wir sehen im Code, dass der Parameter ,,self'' nur bei der Definition einer Methode erscheint. Beim Aufruf wird er nicht angegeben. Im Vergleich zu Funktionsaufrufen ist das zunächst einmal befremdlich, d.h. wir definieren eine Methode mit einem Paramter ,,self'' und rufen sie scheinbar ohne Parameter auf. Aber wenn wir genauer auf den Aufruf schauen, sehen wir, dass wir ja nicht nur SageHallo() aufrufen, sondern dass die Instanz x vor dem Punkt erscheint. Darin liegt das Geheimnis: Es ist gewissermaßen so, als hätten wir SageHallo(x) aufgerufen. In anderen Worten, wir übergeben eine Referenz auf die Instanz x an self.

Zum weiteren Verständnis: Eigentlich müsste man die Methode einer Klasse über den Klassennamen aufrufen. In diesem Fall wird die Instanz als Argument übergeben, also Roboter.SageHallo(x). Weil dies aber zum einen besonders unhandlich ist und außerdem den nicht üblichen Gepflogenheiten in der OOP entspricht, bindet Python alle Methoden automatisch an die Klasseninstanzen.

Instanzvariablen

Wir wollen nun unsere Methode SageHallo so ändern, dass sie sich mit ,,Hallo, mein Name ist Marvin'' meldet, wenn der Roboter ,,Marvin'' heißt. Damit sind wir wieder zurück bei den Instanzattributen. Denn eine Instanz muss sich ihren Namen merken können. In unserem anfänglichen Beispiel definierten wir Attribute außerhalb der Klassendefinition mit Anweisungen der Art x.name = "Marvin", x.baujahr = 1979 und y.name = "Caliban". Wir können gewissermaßen das Gleiche innerhalb einer Klassendefinition in den Methoden tun. Allerdings haben wir dort natürlich keine Ahnung von dem Instanznamen, also x oder y. Wir müssen diesen Namen jedoch nicht wissen, da es sich ja beim formalen Parameter self um eine Referenz auf die aktuelle Instanz handelt. Mit diesem Wissen schreiben wir nun weitere Funktionen, mit denen wir unseren Roboter mit Namen (SetzeNamen) und Baujahr (SetzeBaujahr) versehen können:

class Roboter:
    def SageHallo(self):
        print("Hallo, mein Name ist " + self.name)
    def SetzeNamen(self, name):
        self.name = name
    def SetzeBaujahr(self, baujahr):
        self.baujahr = baujahr
        
x = Roboter()
x.SetzeNamen("Marvin")
x.SetzeBaujahr(1979)
y = Roboter()
y.SetzeNamen("Caliban")
y.SetzeBaujahr(1993)
x.SageHallo()
y.SageHallo()
Hallo, mein Name ist Marvin
Hallo, mein Name ist Caliban


Zu der Methode SetzeNamen gibt es noch Folgendes zu sagen, was auch zwei häufig gestellte Fragen beantwortet:

  • Ja, statt "self" darf man irgendeinen beliebigen Namen verwenden, so auch beispielsweise "this". Man sollte dies jedoch nicht tun, weil man damit eine Python-Konvention verletzt. Außerdem werden andere Programmierer möglicherweise Schwierigkeiten haben, den Code zu verstehen. Manche Entwicklungsumgebungen, wie beispielsweise eclipse, geben auch eine Warnung, wenn man sich nicht an die diese Konvention hält.
  • Der Attributnamen muss nicht gleich dem Namen des formalen Parameters sein. Häufig hat er jedoch den gleichen Namen. Wir hätten den Parameter auch beispielsweise ,,n'' nennen können.

Also ist auch der folgende Python-Code lauffähig, aber keinesfalls empfehlenswert:

class Roboter:
    def SageHallo(self):
        print("Hallo, mein Name ist " + self.name)
    def SetzeNamen(this, n):
        this.name = n
    def SetzeBaujahr(self, baujahr):
        self.baujahr = baujahr
  
      
x = Roboter()
x.SetzeNamen("Marvin")
x.SetzeBaujahr(1979)
y = Roboter()
y.SetzeNamen("Caliban")
y.SetzeBaujahr(1993)
x.SageHallo()
y.SageHallo()
Hallo, mein Name ist Marvin
Hallo, mein Name ist Caliban

Besonders hässlich an obigem Code ist, dass wir einmal this und einmal self verwendet haben, also dass wir nicht konsequent in unser Namensgebung waren.

Zum Stil gibt es auch noch etwas zu sagen: Laut PEP8, dem offiziellen ,,Style-Guide'', gilt: ,,Methodendefinitionen innerhalb einer Klasse werden durch eine einzelne Leerzeile getrennt.'' (Im englischen Original: ,,Method definitions inside a class are separated by a single blank line.'') Dies wird aber in vielen Fällen nicht eingehalten. Selbst die offizielle Python-Dokumentation unter python.org ist hier nicht eindeutig! Wir werden uns im Folgenden auch nicht immer an diese Konvention halten, vor allen Dingen um Platz zu sparen.

Aber neben Stil und der Namensgebung für den ersten Parameter gibt es noch ein viel schwerwiegenderes Problem. Wenn wir einen Roboter neu schaffen, müssen wir jedesmal drei Anweisungen durchführen. In unserem Beispiel sind das, die Instanziierung mittels x = Roboter(), die Namensgebung x.SetzeNamen("Marvin") und das Setzen des Baujahres x.SetzeBaujahr(1979). Dieses Vorgehen ist umständlich, fehlerträchtig und vor allen Dingen entspricht es nicht dem üblichen Vorgehen in der OOP.

Die ```__init__```-Methode

Wir wollen die Attribute sofort nach der Erzeugung einer Instanz definieren. __init__ ist eine Methode, die unmittelbar und automatisch nach der Erzeugung einer Instanz aufgerufen wird. Dieser Name ist festgelegt und kann nicht frei gewählt werden! __init__ gehört zu den sogenannten magischen Methoden, von denen wir in den folgenden Kapiteln noch weitere kennenlernen werden. Die __init__-Methode dient der Initialisierung einer Instanz. Python besitzt keinen expliziten Konstruktor bzw. Destruktor, wie man sie in Java oder C++ kennt. Der eigentliche Konstruktor wird implizit von Python gestartet und __init__ dient, wie der Name andeutet der Initialisierung der Attribute. Die __init__-Methode wird jedoch unmittelbar nach dem eigentlichen Konstruktor gestartet, und dadurch entsteht der Eindruck, als handele es sich um einen Konstruktor. Die __init__-Methode kann an beliebiger Stelle in der Klassendefinition stehen, sollte aber nach Konvention immer die erste Methode direkt unter dem Klassenheader sein.

class A:
    def __init__(self):
        print("__init__ wurde ausgeführt!")
x = A()
__init__ wurde ausgeführt!

Wir schreiben nun für unsere Beispielklasse eine Initialisierungsmethode __init__, um bei der Instanzierung den Namen und das Baujahr setzen bzw. übergeben zu können, also x = Roboter("Marvin", 1979).

class Roboter:
    def __init__(self, name, baujahr):
        self.name = name
        self.baujahr = baujahr
    def SageHallo(self):
        print("Hallo, mein Name ist " + self.name)
    def NeuerName(self, name):
        self.name = name
    def NeuesBaujahr(self, baujahr):
        self.baujahr = baujahr
     
  
x = Roboter("Marvin", 1979)
y = Roboter("Caliban", 1993)
x.SageHallo()
y.SageHallo()
Hallo, mein Name ist Marvin
Hallo, mein Name ist Caliban

Schreibt man x = Roboter("Marvin", 1979), dann verhält sich das logisch gesehen so, als würde man erst eine Instanz x instanziieren, also x = Roboter(), - was natürlich nicht mehr wegen unserer __init__-Methode funktioniert, die zwei Argumente beim Aufruf zwingend fordert! -, und dann die __init__-Methode aufrufen mit x.__init__("Marvin", 1979). (Allerdings lässt sich die __init__-Methode nicht von außen aufrufen, da sie mit einem doppelten Unterstrich beginnt und es sich damit um eine private Methode handelt.)

Der Benutzung der Klasse sieht nun deutlich ,,aufgeräumter'' und klarer aus, aber wir verletzen noch das Prinzip der Datenkapselung bzw. Datenabstraktion, auf das wir im Folgenden eingehen werden.

Im Folgenden wollen wir noch eine Fehlermeldung demonstrieren, die oft bei Anfängern Fragen aufwirft. Wir versuchen in der folgenden interaktiven Python-Shell einen Roboter zu erzeugen. Statt der korrekten Instanziierung x = Roboter("Marvin", 1979) rufen wir Roboter ohne Parameter auf, was nun natürlich zu einer aussagekräftigen Fehlermeldung führt:

x = Roboter()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-19-0b771a5ff2ee> in <module>
----> 1 x = Roboter()
TypeError: __init__() missing 2 required positional arguments: 'name' and 'baujahr'

Datenkapselung, Datenabstraktion und Geheimnisprinzip

Begriffsbestimmungen



Unter Datenkapselung versteht man den Schutz von Daten bzw. Attributen vor dem unmittelbaren Zugriff. Der Zugriff auf die Daten bzw. Attribute erfolgt nur über entsprechende Methoden, die man auch als Zugriffsmethoden bezeichnet. Im allgemeinen gibt es zu einem bestimmten Attribut eine Methode, die einem den Wert des Attributes liefert - häufig als Getter (Englisch ,,to get'' im Sinne von etwas ,,holen'' oder ,,besorgen'') oder als Abfragemethode bezeichnet - und eine andere, mit deren Hilfe man den Wert eines Attributes verändern kann - häufig als Setter (Englisch ,,to set'' im Sinne von etwas ,,festsetzen'', ,,festlegen'' oder ,,einstellen'') oder als Änderungsmethode benannt.

Datenabstraktion = Datenkapselung + Geheimnisprinzip

Wenn wir von ,,Schutz von Daten bzw. Attributen vor dem unmittelbaren Zugriff'' reden, dann bedeutet das natürlich nicht automatisch auch, dass die Daten außen nicht sichtbar sind. In der OOP ist jedoch auch dies gefordert. Man spricht in diesem Zusammen auch häufig vom Geheimnisprinzip (Im Englischen unter ,,Information Hiding'' bekannt). Unter dem Geheimnisprinzip versteht man das Verbergen von internen Informationen und Implementierungsdetails nach außen. Eine andere Sichtweise des Geheimnisprinzips besagt, dass die Benutzer einer Klasse nicht mit unnötigen Implementierungsdetails belastet werden. Die außen unbedingt nötigen Daten werden nur über definierte Schnittstellen bzw. Methoden nach außen sichtbar und benutzbar gemacht. So gesehen beleuchten Datenkapselung und Geheimnisprinzip die zwei Seiten einer Medaille. Um beiden Seiten gerecht zu werden, benutzen viele deshalb auch den allgemeineren Begriff ,,Datenabstraktion''. Man könnte folgende ,,Gleichung'' aufstellen:

Datenabstraktion = Datenkapselung + Geheimnisprinzip

Allerdings werden die drei Begriffe in der Literatur zumeist synonym verwendet, was zu einem bestimmten Grad auch für dieses Buch gilt.
Wie sieht es nun mit der Datenabstraktion in unserer Beispielklasse aus? Betrachten wir dazu die folgende interaktive Pythonsession.

x = Roboter("Marvin", 1979)
print(x.name)
x.name = "Caliban"
print(x.name)
Marvin
Caliban

Wir sehen mit der print-Funktion, dass wir lesend auf das Attribut ,,name'' zugreifen können. Wir können auch den Wert des Attributes direkt von außen, also von unserer interaktiven Pythonshell aus, ändern: x.name = "Caliban". Korrekt im Sinne der OOP wäre es jedoch gewesen die Methode NeuerName aufzurufen:

x = Roboter("Marvin", 1979)
x.NeuerName("Caliban")

Bei der print-Funktion stellt sich noch ein weiteres Design-Problem. Wie können wir uns den Namen unseres Roboters ausgeben lassen, ohne auf das Attribut direkt zuzugreifen? Wir können dies natürlich mit einer neuen Methode realisieren, die uns den Namensstring zurückliefert. Wir nennen diese HoleNamen. Weil wir dies natürlich auch für das Baujahr benötigen, schreiben wir auch hierfür eine Methode HoleBaujahr.

Unsere neue Roboterklasse sieht nun wie folgt aus:

class Roboter:
    def __init__(self, name, baujahr):
        self.name = name
        self.baujahr = baujahr
    def SageHallo(self):
        print("Hallo, mein Name ist " + self.name)
    def NeuerName(self, name):
        self.name = name
    def HoleNamen(self):
        return self.name
    def NeuesBaujahr(self, baujahr):
        self.baujahr = baujahr
    def HoleBaujahr(self):
        return str(self.baujahr)
x = Roboter("Marvin", 1979)
y = Roboter("Caliban", 1993)
for rob in [x, y]:
    rob.SageHallo()
    print("Ich bin " + rob.HoleBaujahr() + " erschaffen worden! ")
Hallo, mein Name ist Marvin
Ich bin 1979 erschaffen worden! 
Hallo, mein Name ist Caliban
Ich bin 1993 erschaffen worden! 

Die ```__str__```- und die ```__repr__```-Methode

Wir verlassen für dieses Unterkapitel unser Thema Datenkapselung, da wir noch auf zwei wichtige Methoden eingehen müssen, die wir bei den folgenden Klassendefinitionen benutzen werden. Im Laufe des Tutorials hatten wir bereits die Funktion ,,str'' kennengelernt. Wir hatten gesehen, dass wir mit ihr verschiedenste Datentypen als Strings darstellen konnten. Prinzipiell macht ,,repr'' genau dasselbe, d.h. es wandelt einen Datentyp in seine Stringdarstellung.

lst = ["Python", "Java", "C++", "Perl"]
print(lst)
str(lst)
['Python', 'Java', 'C++', 'Perl']
Ausgabe: :

"['Python', 'Java', 'C++', 'Perl']"
In [ ]:
repr(lst)
d = {"a":3497, "b":8011, "c":8300}
print(d)
str(d), repr(d)
{'a': 3497, 'b': 8011, 'c': 8300}
Ausgabe: :

("{'a': 3497, 'b': 8011, 'c': 8300}", "{'a': 3497, 'b': 8011, 'c': 8300}")
x = 587.78
str(x), repr(x)
Ausgabe: :

('587.78', '587.78')

Wendet man auf ein Objekt die Funktion str oder repr an, sucht Python in der Klassendefinition nach Methoden mit den Namen __str__ und __repr__. Sind sie vorhanden werden sie entsprechend aufgerufen. Im Folgenden definieren wir eine Klasse A, in der wir weder __repr__ noch __str__ definieren. In diesem Fall wird sowohl bei str und repr, aber auch bei einem einfachen print oder der direkten Ausgabe über die interaktive Shell, eine Default-Ausgabe gewählt:

class A:
    pass
a = A()
print(repr(a))
<__main__.A object at 0x7f3fd4bf6b10>
print(str(a))
<__main__.A object at 0x7f3fd4bf6b10>
a
Ausgabe: :

<__main__.A at 0x7f3fd4bf6b10>

Besitzt eine Klasse eine Funktion __str__, dann wird diese angewendet, wenn auf ein Objekt dieser Klasse str angewendet wird, oder wenn ein Objekt dieser Klasse in einer print-Funktion verwendet wird. __str__ wird jedoch nicht angewendet, wenn man repr auf ein Objekt anwendet, oder wenn man sich direkt den Wert eines Objektes angeben lässt:

class A:
    def __str__(self):
        return "42"
a = A()
print(repr(a))
<__main__.A object at 0x7f3fd4bf2d10>
print(a)
42
print(str(a))
42

Besitzt eine Klasse nur eine Funktion repr aber nicht __str__, dann wird __repr__ immer angewendet, d.h. also wenn auf ein Objekt dieser Klasse str und repr angewendet werden, oder wenn ein Objekt dieser Klasse in einer print-Anweisung oder direkt in der Python-Shell für eine Ausgabe verwendet wird:

class A:
    def __repr__(self):
        return "42"
a = A()
print(repr(a))
print(str(a))
42
42

Eine häufig in Foren gestellte Frage lautet, wann man __repr__ und wann __str__ benutzen sollte. __str__ ist immer dann die richtige Methode, wenn es um die Ausgabe der Daten für Endbenutzer geht. __repr__ hingegen ist wichtig, wenn es um die intern-Darstellung von Daten geht. Wendet man auf ein Objekt "o" die Funktion repr an, dann erhält man eine Stringrepäsentation des Objektes "o", aus der man durch Anwendung der Funktion eval wieder das Objekt "o" erzeugen kann, genauer gesagt erzeugt man natürlich eine Kopie des ursprünglichen Objektes o.
Es gilt also:

type(o) == type(eval(repr(o)))

Eigentlich müsste auch o == eval(repr(o)) gelten, aber da wir noch keine Vergleichsmethoden für unsere Klassenobjekte definiert haben, wird nur die Gleichheit auf die Objekt-ID geprüft, d.h. Kopien von Objekten gelten also als Ungleich.

Obige Typ-Gleichheit gilt nicht in jedem Fall, wenn man statt repr str verwendet. Das Hauptaugenmerk bei str liegt auf der Lesbarkeit. Benutzer sollen einen verständlichen String erhalten! Bei __repr__ liegt der Fokus auf der Eindeutigkeit und Unmissverständlichkeit der Datendarstellung!

Das bisher Gesagte wollen wir nochmals an einem Beispiel verdeutlichen. Dazu benutzen wir das datetime-Modul:

import datetime
today = datetime.datetime.now()
str_s = str(today)
eval(str_s)
Traceback (most recent call last):
  File "/home/bernd/anaconda3/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3319, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-33-52b036e82a64>", line 4, in <module>
    eval(str_s)
  File "<string>", line 1
    2021-02-17 18:06:26.723484
          ^
SyntaxError: invalid token

Wir sehen, dass wir den String repr_s , den wir mittels repr erhalten wieder mit eval in ein datetime.datetime-Objekt wandeln können. Den durch str erzeugten String str_s kann man jedoch nicht mehr rückwandeln.

Wir können nun unsere Roboterklasse um eine repr-Methode erweitern. Um die Darstellung übersichtlich zu halten, haben wir im folgenden Code alle anderen vorher definierten Methoden weggelassen:

class Roboter:
    
    def __init__(self, name, baujahr):
        self.name = name
        self.baujahr = baujahr
    def __repr__(self):
        return "Roboter(\"" + self.name + "\"," +  str(self.baujahr) +  ")"
 
       
x = Roboter("Marvin", 1979)
x_str = str(x)
print(x_str)
print("Typ von x_str: ", type(x_str))
neu = eval(x_str)
print(neu)
print("Typ von neu:", type(neu))
Roboter("Marvin",1979)
Typ von x_str:  <class 'str'>
Roboter("Marvin",1979)
Typ von neu: <class '__main__.Roboter'>

Die Anweisung print(x_str) gibt den String Roboter("Marvin",1979) aus. Wir können diesen String wieder mittels eval(x_str) in eine Instanz neu der Klasse Roboter wandeln.

Starten wir das Skript erhalten wir folgende Ausgaben:

Roboter("Marvin",1979)
Typ von x_str:  <class 'str'>
Roboter("Marvin",1979)
Typ von neu: <class '__main__.Roboter'>

Nun erweitern wir unsere Klasse noch um eine benutzerfreundliche __str__-Methode. Wir sehen, dass wir die durch __repr__ erzeugten Strings wieder mittels eval in ein Roboter-Objekt wandeln können:

class Roboter:
    
    def __init__(self, name, baujahr):
        self.name = name
        self.baujahr = baujahr
    def __repr__(self):
        return "Roboter(\"" + self.name + "\"," +  str(self.baujahr) +  ")"
  
    def __str__(self):
        return "Name: " + self.name + ", Baujahr: " +  str(self.baujahr)
        
x = Roboter("Marvin", 1979)
x_str = str(x)
print(x_str)
print("Typ von x_str: ", type(x_str))
x_repr = repr(x)
print(x_repr, type(x_repr))
neu = eval(x_repr)
print(neu)
print("Typ von neu:", type(neu))
Name: Marvin, Baujahr: 1979
Typ von x_str:  <class 'str'>
Roboter("Marvin",1979) <class 'str'>
Name: Marvin, Baujahr: 1979
Typ von neu: <class '__main__.Roboter'>

Public- Protected- und Private-Attribute

No Trespassing

Wer kennt nicht die Klischees in Filmen und Erzählungen von schießwütigen Farmern, die sofort losschießen, wenn jemand ihr Grundstück betritt. Natürlich hat diese Person willentlich oder auch versehentlich das ansonsten kaum zu übersehene Schild mit der Aufschrift ,,Private NO Trespassing'' nicht beachtet. Besonders irritierend ist dabei häufig die Tatsache, dass die Grundstücke in keinster Weise eingezäunt sind. Aber es gibt auch die Variante, in denen die Grundstücke mit hohen Zäunen, manchmal gar mit Stacheldraht gesichert sind. Aber egal ob der Zugang durch bauliche Maßnahmen erschwert wurde oder nicht, sobald man ein solches Grundstück betritt begeht man Hausfriedensbruch. (Naja, ist sicherlich nicht ganz juristisch korrekt und wir haben auch bewusst offen gelassen, um welches Land oder welchen Staat es geht.)

Im Gegensatz zum Privatbesitz, zu dem einem der Zutritt verwehrt werden kann, gibt es dann auch öffentliche Räume, wie Straßen oder Plätze, die von allen genutzt werden dürfen.

Auf eigene Gefahr Dazwischen gibt es noch eine besondere Form: Privatbesitz darf genutzt werden, aber nur auf eigene Gefahr. Sie ahnen es sicherlich schon. Wir brauchen diese Vorstellungen von Zugangsberechtigungen für unsere Attribute.

Wählt man Namen ohne führenden oder führende Unterstriche als Attribute einer Klasseninstanz, so sind diese öffentlich zugänglich, d.h. von außen, also außerhalb der Klassendefinition. Wichtig ist vor allen Dingen die Tatsache, dass sie nicht nur von außen, also von den Benutzern der Klasse, genutzt werden können, sondern dass es sich bei dieser Nutzung um einen ordnungsgemäßen Gebrauch der Klasse handelt.
Möchte man verhindern, dass ein Attribut lesend oder schreibend von den Benutzern der Klasse genutzt werden kann, gibt es zwei Möglichkeiten, die Python einem zur Verfügung stellt. Jedes Attribut, welches mit genau einem Unterstrich beginnt, ist ,,protected''. In diesem Fall kann man zwar immer noch von außen lesend und schreibend auf das Attribut zugreifen, aber durch den Unterstrich hat man klar gemacht, dass dies ,,verboten'' oder ,,nicht erwünscht'' ist. Dies entspricht in etwa unserem ,,No Trespassing''-Schild auf einem Grundstück, dass ansonsten keinerlei bauliche Maßnahmen hat, die ein Eindringen verhindern. Die ,,baulichen Maßnahmen'' erfolgen erst, wenn man den Namen eines Attributes mit zwei Unterstrichen beginnen lässt. Ein solches Attribut ist ein ,,private''-Attribut, auf das von außen nicht zugriffen werden kann.3

In der folgenden Tabelle haben wir die verschiedenen Attributarten nochmals zusammengefasst:

Namen
Bezeichnung
Bedeutung
name Public
Attribute ohne führende Unterstriche sind sowohl innerhalb einer Klasse als auch von außen les- und schreibbar.
_name Protected
Man kann zwar auch von außen lesend und schreibend zugreifen, aber der Entwickler macht damit klar, dass man diese Member nicht benutzen sollte. Protected-Attribute sind insbesondere bei Vererbungen von Bedeutung.
__name Private
Sind von außen nicht sichtbar und nicht benutzbar.



Wir wollen uns das Verhalten der verschiedenen Attribute in einer Beispielklasse anschauen:



</pre>
Obige Beispielklasse speichern wir unter attributes.py und testen sie in der interaktiven Python-Shell:

from attributes import A

Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'A' object has no attribute '__priv' </pre>

Interessant ist vor allen Dingen die Fehlermeldung

AttributeError: 'A' object has no attribute '__priv' 


Eigentlich würde man als Fehlermeldung erwarten, dass man auf das Attribut priv nicht zugreifen darf, da es ,,private'' ist. Stattdessen kommt die obige Meldung, die so tut, als gäbe es kein Attribut priv in der Klasse A. Dies ist wichtig, da ansonsten Benutzer der Klasse Code programmieren könnten, der auf der Existenz bzw. Nicht-Existenz von ,,private''-Attributen beruht. Damit wäre auch das Prinzip der Datenkapselung verletzt.

Bevor Sie mir der Lektüre fortfahren, können Sie sich überlegen, wie man die bisherige Roboterklasse mit privaten Attributen für den Namen und das Baujahr umschreibt.

Dazu muss man jedes Vorkommen von self.name und self.baujahr durch self.name und self.baujahr ersetzen.

Das folgende Listing zeigt die Klasse mit privaten Attributen:

class A():
    
    def __init__(self):
        self.__priv = "Ich bin privat"
        self._prot = "Ich bin protected"
        self.pub = "Ich bin öffentlich"
        
x = A()
x.pub 
Ausgabe: :

'Ich bin öffentlich'
x.pub = "Man kann meinen Wert ändern und das ist gut so"
x.pub 
Ausgabe: :

'Man kann meinen Wert ändern und das ist gut so'
x._prot
Ausgabe: :

'Ich bin protected'
x._prot = "Man Wert kann aber sollte nicht von außen geändert werden!"
x._prot
Ausgabe: :

'Man Wert kann aber sollte nicht von außen geändert werden!'
x.__priv
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-45-f75b36b98afa> in <module>
----> 1 x.__priv
AttributeError: 'A' object has no attribute '__priv'

Wir schreiben nun eine Klasse mit privaten Attributen:

class Roboter:
    
    def __init__(self, name, baujahr):
        self.__name = name
        self.__baujahr = baujahr
    def __repr__(self):
        return "Roboter(\"" + self.__name + "\"," +  str(self.__baujahr) +  ")"
  
    def __str__(self):
        return "Name: " + self.__name + ", Baujahr: " +  str(self.__baujahr)
    
    def SageHallo(self):
        print("Hallo, mein Name ist " + self.__name)
        
    def NeuerName(self, name):
        self.__name = name
        
    def HoleNamen(self):
        return self.__name
    
    def NeuesBaujahr(self, baujahr):
        self.__baujahr = baujahr
        
    def HoleBaujahr(self):
        return str(self.__baujahr)
        
x = Roboter("Marvin", 1979)
y = Roboter("Caliban", 1993)
for rob in [x, y]:
    rob.SageHallo()
    print("Ich bin " + rob.HoleBaujahr() + " erschaffen worden! ")
    print(rob)
Hallo, mein Name ist Marvin
Ich bin 1979 erschaffen worden! 
Name: Marvin, Baujahr: 1979
Hallo, mein Name ist Caliban
Ich bin 1993 erschaffen worden! 
Name: Caliban, Baujahr: 1993

Wir sehen, dass wir für beide privaten Attribute, also __name und __baujahr, jeweils einen Getter (HoleNamen(self) und HoleBaujahr(self)) und einen Setter (NeuerNamen(self) und NeuesBaujahr(self)) zur Verfügung gestellt haben. Nicht jedes private Attribut braucht oder sollte einen Getter oder Setter haben. Dies ist eine Designfrage: Eine Klasse wird unübersichtlich, wenn man ihr zuviele unnötige und vielleicht sogar unnütze Methoden zur Verfügung stellt. Es gibt IDEs, die stellen automatisch zu jedem privaten Attribut einen Getter und einen Setter zur Verfügung. Also für ein privates Attribut self.__x werden dann automatisch eine Getter- und eine Setter-Methode zur Verfügung gestellt. Diese sehen dann prinzipiell so aus:

class A():
    
    def __init__(self, x, y):
        self.__x = x
        seld.__y = y
    def GetX(self):
        return self.__x
    def GetY(self):
        return self.__y
    def SetX(self, new):
        self.__x = new
    def SetY(self, new):
        self.__y = new

Dies ist aus zweierlei Gründen nicht sinnvoll. Erstens benötigt man für viele privaten Attribute, wie bereits erwähnt, von außen keinen Zugriff und zweitens erzeugt dies einen nicht-pythonischen Code. Wir werden später sehen, wie man dies besser gestaltet.

Destruktor

Für eine Klasse kann man auch die Methode __del__ definieren. Sie wird aufgerufen, bevor eine Instanz zerstört wird. Er wird häufig auch als Destruktor bezeichnet, obwohl es sich eigentlich nicht um den Destruktor handelt. Wenn man eine Instanz einer Klasse mit del löscht, wird die Methode __del__ vor dem eigentlichen Destruktor aufgerufen. Allerdings nur, falls es keine weitere Referenz auf diese Instanz gibt. Destruktoren werden selten benutzt, da man sich normalerweise nicht um das Aufräumen im Speicher kümmern muss.

Im Folgenden sehen wir ein Beispiel mit __init__ und __del__:

class Roboter():
    
    def __init__(self, name):
        print(name + " wurde erschaffen!")
        
    def __del__(self):
        print ("Roboter wurde zerstört")
        
        
x = Roboter("Tik-Tok")
y = Roboter("Jenkins")
z = x
print("Deleting x")
del x
print("Deleting z")
del z
del y
Tik-Tok wurde erschaffen!
Jenkins wurde erschaffen!
Deleting x
Deleting z
Roboter wurde zerstört
Roboter wurde zerstört

Die Verwendung der __del__-Methode ist sehr problematisch. Kommt man auf die Idee, den Robotern ,,ein persönliches Ende'' zu bereiten, wie im folgenden Programm, erhalten wir eine Fehlermeldung:

class Roboter():
    
    def __init__(self, name):
        print(name + " wurde erschaffen!")
        
    def __del__(self):
        print(self.name + " sagt bye-bye. ")
        print("Es gibt " + self.name + " ihn nun nicht mehr!")
        
        
x = Roboter("Tik-Tok")
y = Roboter("Jenkins")
z = x
print("Deleting x")
del x
print("Deleting z")
del z
del y
Tik-Tok wurde erschaffen!
Jenkins wurde erschaffen!
Deleting x
Deleting z
Exception ignored in: <function Roboter.__del__ at 0x7f3fd4be97a0>
Traceback (most recent call last):
  File "<ipython-input-49-8a66dd556b63>", line 7, in __del__
AttributeError: 'Roboter' object has no attribute 'name'
Exception ignored in: <function Roboter.__del__ at 0x7f3fd4be97a0>
Traceback (most recent call last):
  File "<ipython-input-49-8a66dd556b63>", line 7, in __del__
AttributeError: 'Roboter' object has no attribute 'name'

Wir greifen auf das Attribut ,,name'' zu, was aber in __del__ in diesem Fall bereits nicht mehr vorhanden ist. Wir werden später nochmals intensiv auf diese Problematik eingehen.

Fußnoten:

1
Nach der offiziellen Python-Referenz wird in Python eigentlich alles als Objekt bezeichnet, also beispielsweise Zahlen, Strings, Listen, aber auch Funktionen und Module. Im englischen Original heißt es dazu: ,,Objects are Python's abstraction for data. All data in a Python program is represented by objects or by relations between objects. (In a sense, and in conformance to Von Neumann's model of a "stored program computer", code is also represented by objects.) Every object has an identity, a type and a value.

2
Der Begriff Attribut stammt vom lateinischen ,,attribuere'', was ,,zuweisen'' oder ,,zuteilen'' bedeutet. Der Begriff Attribut wird übrigens auch in der Philosophie verwendet, wo es die einem Gegenstand oder Objekt zugewiesene Eigenschaft bezeichnet.

3
Es gibt jedoch einen Weg, wie man dennoch von außen zugreifen kann: Auf ein Attribut, wie in unserem Beispiel __baujahr, kann man für eine Instanz x mittelsx._Roboter__baujahr``` zugreifen. Dies sollte man jedoch keinesfalls tun!