Klassen
Motivation
Listen und Dictionaries sind flexible und mächtige Datenstrukturen mit denen man schon relativ weit kommen kann. Trotzdem möchte man manchmal Daten so gruppieren, dass man zusammengehörige Daten auch an derselben Stelle hat.
Stellen Sie sich vor, Sie vollen die Daten von verschiedenen Autos verwalten und Sie interessieren sich aktuell für die Leistung in kW und die Höchstgeschwindigkeit in km/h. Dann müssten Sie mit Ihrem aktuellen Wissen wie folgt vorgehen:
porsche_leistung = 427 # kW
porsche_vmax = 320 # km/h
panda_leistung = 51 # kW
panda_vmax = 164 # km/h
Sie könnten die Daten auch in einem Dictionary speichern:
autos = { "porsche": { "leistung": 427, "vmax": 320},
"panda": { "leistung": 51, "vmax": 164} }
Keine der Lösungen ist aber besonders schön. Die Verwendung von Dictionaries hat den Nachteil, dass hier mit Strings gearbeitet wird und schon ein kleiner Tippfehler zu Problemen führt: autos["porsche"]["Leistung"] → "KeyError: 'Leistung'".
- Bisher haben wir eine Reihe von Objekten in Python benutzt
- Integer
- Strings
- Listen
- Dictionaries
- etc.
- Man will aber im allgemeinen komplexere Strukturen aufbauen können
Was wir wollen, sind komplexere Strukturen, die alle Daten zusammenhalten. Wir hätten gerne etwas, mit dem wir Autos verwalten können, die eine Leistung und eine Höchstgeschwindigkeit haben.
Der Traum wäre:
porsche = Auto(leistung = 427, vmax = 320)
print(porsche.leistung) # -> 427
print(porsche.vmax) # -> 320
Im Folgenden wollen wir sehen, wie man solche Datenstrukturen bauen kann.
Objekt und Klasse
Für die weitere Diskussion sollen zwei Begriffe eingeführt werden: Objekt und Klasse.
- Ein Objekt (object) repräsentiert ein einzelnes, konkretes Exemplar
- Auto mit dem Kennzeichen UN-IX 1970
- Brad Pitt
- BluRay mit dem Film "Pulp Fiction" in meinem Regal
- Mein Laptop mit der MAC-Adresse A0:FF:E3:09:6E:34
- Objekte lassen sich Klassen zuordnen
- Fahrzeuge der Marke Renault, Modell Clio
- Schauspieler
- BluRays des Film "Pulp Fiction"
- MacBook Pro
Für das Verständnis der objektorientierten Programmierung ist der Unterschied zwischen Objekt und Klasse entscheidend. Das Objekt repräsentiert ein einzelnes Exemplar einer bestimmten Art von Objekten. Die Klasse hingegen fasst die Gemeinsamkeiten aller Objekte zusammen, die sich unter einem Oberbegriff zusammenfassen lassen.
So ist z. B. Brad Pitt ein konkretes Objekt, denn es gibt – soweit wir wissen – nur einen einzigen Menschen mit diesem Namen, der als Schauspieler weltbekannt ist und so aussieht wie Tyler Durden aus dem Film "Fight Club". Es mag zwar noch andere Menschen geben, die "Brad Pitt" heißen, aber es handelt sich dann um andere Personen und nicht den Schauspieler, den wir normalerweise mit dem Namen verbinden. Der Name ist also hier nicht eindeutig, das Objekt (die Person) aber schon.
Neben Brad Pitt gibt es noch eine Reihe von weiteren Schauspielern mit Weltruhm, z. B. George Clooney. Wir wissen sicher, dass George Clooney und Brad Pitt unterschiedliche Menschen sind und beide teilweise in denselben Filmen auftreten, z. B. in "Ocean's Eleven". Insofern sind Brad Pitt und George Clooney unterschiedliche Objekte, obwohl sie viele Gemeinsamkeiten haben. Trotz ihrer Unterschiede haben sie aber so viele Gemeinsamkeiten, dass wir uns erlauben sie beide mit Begriffen wie "Mensch", "Schauspieler", "Superstar" etc. zu bezeichnen.
Die Programmierung wäre langweilig, wenn man nur mit Objekten hantieren würde, sondern die Gemeinsamkeiten von Objekten sollen sich in einer Weise darstellen lassen, dass wir Objekte derselben Kategorie auf dieselbe Art und Weise behandeln können. Wir nennen diese Kategorien, in die sich Objekte einordnen lassen Klassen.
- Klasse (class) beschreibt den Typ von Objekten (führen neuen Datentyp ein)
- Bauplan (Schablone) für gleichartige Objekte
- Konstruktionszeichnung für einen Auto
- Tierrasse in der Biologie
- Fasst damit gleichartige Objekte zusammen
- gleichen Eigenschaften (Attributen)
- gleichem Verhalten (Methoden)
- Attribute und Methoden bilden eine Einheit
Eine Klasse fasst nun die Gemeinsamkeiten von Objekten zusammen. So wie wir Brad Pitt und George Clooney mit dem Begriff "Schauspieler" bezeichnen können, fasst die Klasse Schauspieler die gemeinsamen Eigenschaften von beiden (und allen anderen Schauspielern) zusammen. Ein Schauspieler zeichnet sich durch einen Künstlernamen, eine Liste von Filmen in denen er mitgespielt hat etc. aus.
In der Programmierung dient eine Klasse dazu, Objekte über deren Gemeinsamkeiten zu beschreiben. Anstatt also jedes Objekt einzeln detailliert zu definieren, erzeugt man mit der Klasse eine Schablone, mit der neue Objekte erzeugt werden können.
Man kann sich eine Klasse wie einen Stempel vorstellen und die Objekte als die Stempelabdrücke. Es gibt nur einen Stempel (die Klasse) aber beliebig viele Abdrücke (die Objekte).
In der Programmierung werden in einer Klasse nicht nur die Eigenschaften beschrieben, die wir Attribute nennen, sondern auch das Verhalten der Objekte. Unter Verhalten verstehen wir die Aktionen, die Objekte ausführen können und die wir mit den Objekten durchführen können. Das Verhalten manifestiert sich in Form von Methoden, die das Objekt trägt und die auf den Attributen des Objekts operieren.
Eine Klasse als konkreter Gegenstand der Programmierung hat verschiedene Bestandteile:
- Name, z. B.
Auto - immer im Singular
- großer Anfangsbuchstabe
- Eigenschaften (Attribute), z. B.
vmax - Daten, die alle Objekte der Klasse tragen sollen
- kleiner Anfangsbuchstabe
- Konstruktoren (werden später erst eingeführt)
- Verhalten (Methoden), z. B.
beschleunigen - typische Verhaltensweisen für alle Objekte der Klasse
- kleiner Anfangsbuchstabe
Die Nutzung der Klasse als reiner Datenspeicher ist nur ein erster Schritt. Viel wichtiger ist die Tatsache, dass die Klassen neben den eigentlichen Daten (Attribute) auch das Verhalten der Objekte beschreiben, also Methoden enthalten. Moment, wieso beschreiben sie das Verhalten der Objekte? Eine Klasse ist nur die Schablone, in freier Wildbahn treffen Sie auf die aus der Klasse erzeugten Stempelabdrücke (die Objekte). Insofern beschreiben wir in der Klasse, wie sich die Objekte verhalten.
Später werden noch die Konstruktoren behandelt, die bei der Erzeugung von Objekten einer Klasse eine entscheidende Rolle spielen.
Als unsere erste Klasse legen wir eine leere Klasse für Autos an. Diese nennen wir, naheliegenderweise, Auto.
class Auto:
pass
mein_auto = Auto()
print(mein_auto)
# <__main__.Auto object at 0x1021acb70>
Den pass-Befehl haben Sie bereits kennengelernt: Er hat keine Funktion, befriedigt aber die Syntax von Python, die für eine Klasse immer mindestens einen Befehl fordert.
Der Ausdruck Auto() erzeugt ein neues Objekt von der Klasse Auto. Eine Referenz auf dieses Objekt wird in der Referenzvariable mein_auto gespeichert. Wir müssen streng trennen zwischen dem Objekt und der Variable, die auf es zeigt. Auf ein Objekt können beliebig viele Referenzvariablen zeigen, mindestens aber eine, sonst wird das Objekt automatisch gelöscht.
auto1 = Auto() # Eine Variable zeigt auf das Objekt
auto2 = auto1 # Zwei Variablen zeigen auf das Objekt
auto1 = None # Eine Variable zeigt auf das Objekt
auto2 = None # Keine Variable zeigt auf das Objekt -> wird gelöscht
Wenn man das Objekt ausgibt, d. h. der print()-Funktion übergibt, dann wird die Klasse __main__.Auto und die aktuelle Speicherstelle (0x1021acb70) angezeigt. Das __main__ vor dem Klassennamen bedeutet, dass die Klasse keinen Modul zugeordnet wurde, sodass sie automatisch im Hauptmodul namens __main__ gelandet ist.
Klassen sind die Schablonen für Objekte, d. h. wir können von der Klasse Auto beliebig viele Objekte anlegen, obwohl das aktuell ziemlich sinnlos ist.
class Auto:
pass
fuhrpark = [ Auto(), Auto(), Auto() ]
print(fuhrpark)
# [<__main__.Auto object at 0x7ff8ac1f8340>,
# <__main__.Auto object at 0x7ff8ac1777f0>,
# <__main__.Auto object at 0x7ff8ab881e50>]
Constructor
Unser Auto aus den vorherigen Beispielen ist ausgesprochen langweilig. Wir können Objekte davon anlegen, diese tragen aber überhaupt keine sinnvollen Daten. Wir müssen also irgendwie die für ein Auto relevanten Informationen in die Objekte bekommen. Dies geschieht über den Konstruktor, der bestimmt, welche Daten wir bei der Erzeugung in das Objekt speichern können.
- Der Konstruktor (Constructor) ist eine spezielle Methode, die das Objekt bei der Erzeugung initialisiert
- Die Variablen des Objektes (Attribute) entstehen automatisch bei der ersten Zuweisung
class Auto:
def __init__(self, vmax):
self.vmax = vmax
porsche = Auto(250)
ente = Auto(100)
print(porsche.vmax) # -> 250
print(ente.vmax) # -> 100
In Python hat der Konstruktor den seltsamen Namen __init__ und bekommt immer mindestens einen Parameter, namens self. Dieser Parameter ist eine Referenz auf das aktuell erzeugte Objekt und kann deswegen dazu benutzt werden, die Attribute des Objektes zu initialisieren.
class Selfie:
def __init__(self):
print(self)
a = Selfie()
print(a)
<__main__.Selfie object at 0x7ff8ab1c2ac0>
<__main__.Selfie object at 0x7ff8ab1c2ac0>
Man sieht, dass beide Ausgaben dasselbe Objekt zeigen.
Ein Konstruktor kann zusätzlich zu self weitere Parameter haben, die dann dazu benutzt werden können, Daten im Objekt abzulegen. Natürlich müssen diese nicht immer eins-zu-eins gespeichert werden, sondern können auch im Konstruktor beliebig verarbeitet werden, bevor sie im Objekt gespeichert werden. Weiterhin kann der Konstruktor auch Attribute anlegen, zu denen es keine Parameter gibt. Die Beziehung zwischen Parametern des Konstruktors und Attributen ist daher keine feste, sondern hängt von der Klasse ab.
class Auto:
def __init__(self, ps):
self.leistung = ps * 0.73549875
if self.leistung > 100:
self.fahrspass = "Hoch"
else:
self.fahrspass = "Niedrig"
a1 = Auto(100)
a2 = Auto(200)
print(a1.leistung, a1.fahrspass) # -> 73.549875 Niedrig
print(a2.leistung, a2.fahrspass) # -> 147.09975 Hoch
Das Beispiel zeigt, wie ein Attribut (leistung) aus einem Parameter berechnet wird und ein anderes Attribut (fahrspass) in Abhängigkeit davon gesetzt wird.
Der Konstruktor (__init__) wird von Python immer automatisch gerufen, wenn ein Objekt erzeugt wird.
self
Python hat eine Besonderheit, die es von vielen anderen objektorientierten Programmiersprachen unterscheidet: Bei der Definition von Methoden muss man den Parameter self, der auf das aktuelle Objekt zeigt, explizit mit angeben. Andere Programmiersprachen machen das nicht so, sondern erzeugen den Parameter hinter den Kulissen und stellen ihn dann magisch in der Methode zur Verfügung.
In Ruby zum Beispiel sieht eine Methodendefinition wie folgt aus:
class Auto
def methode()
print(self)
end
end
a = Auto.new()
a.methode() # -> #<Auto:0x0000559d8e6846e8>
In Python muss man den self-Parameter selbst angeben:
class Auto:
def methode(self):
print(self)
a = Auto()
a.methode() # -> <__main__.Auto object at 0x7f3d6c7bf880>
In beiden Programmiersprachen muss man beim Aufruf den Parameter für das aktuelle Objekt nicht angeben, dieser wird automatisch gesetzt. Bei der Definition der Methode muss er aber in Python aufgeführt werden.
- In jeder Methode gibt es eine spezielle Referenzvariable, die auf das eigene Objekt zeigt, die self-Referenz
- Über die self-Referenz greift die Methode auf Attribute des Objektes zu
- Sie verhält sich ähnlich einer normalen Referenzvariable (allerdings keine Zuweisung möglich)
- Anders als in anderen Programmiersprachen, muss
selfexplizit in den Methoden deklariert werden
Das folgende Beispiel zeigt die Verwendung von self im Konstruktor und in einer Methode beschleunigen.
class Auto:
def __init__(self, vmax):
self.vmax = vmax
self.v = 0
def beschleunigen(self):
self.v = self.v + 10
if (self.v > self.vmax):
self.v = self.vmax
ente = Auto(100)
ente.beschleunigen()
print(ente.v) # -> 10
ente.beschleunigen()
print(ente.v) # -> 20
Klassenattribute
Die Attribute, die im Konstruktor erzeugt werden, sind für jedes Objekt individuell, d. h. jedes Objekt kann einen anderen Wert in diesen Variablen haben. Oben wurde das z. B. mit Autos gezeigt, die unterschiedlichen Höchstgeschwindigkeiten haben können.
In manchen Fällen möchte man Attribute nicht pro Objekt speichern, sondern für alle Objekte nur einen einzigen Wert verwalten. Hierzu benutzt man Klassenattribute.
Klassenattribute (class attributes) werden über Instanzen hinweg geteilt
class Auto:
zaehler = 1 # Klassenattribut
def __init__(self):
self.serien_nummer = Auto.zaehler
Auto.zaehler += 1
ente = Auto()
print(ente.serien_nummer) # -> 1
porsche = Auto()
print(porsche.serien_nummer) # -> 2
print(Auto.zaehler) # -> 3
Man sieht in dem Beispiel, dass der Wert von Autozähler über die Objekte hinweg existiert, weil er auf der Ebene der Klasse und nicht der Objekte definiert ist.
Vererbung
Objektorientierte Programmierung wäre nicht so erfolgreich, wenn es die Vererbung nicht gäbe: Man kann neue Klassen von bereits existierenden Klassen erben lassen und damit die Eigenschaften und Methoden der Elternklasse auf die Kindklasse übertragen. Die Kindklasse hat dann die Möglichkeit, die Attribute und Methoden der Elternklasse zu erweitern und zu verfeinern.
- Klassen können Eigenschaften (Methoden und Attribute) an andere Klassen weitergeben (vererben)
- Man hat dann Kindklassen und Elternklassen
class Tier:
def __init__(self, beine):
self.beine = beine
class Hund(Tier): # Hund erbt von Tier
def __init__(self, name):
super().__init__(4)
self.name = name # Hunde haben Namen
def beissen(self):
print(self.name, 'hat dich gebissen')
Die Kindklasse (Subklasse) gibt die Elternklasse (Superklasse) in Klammern hinter ihrem Namen an Hund(Tier). Sie kann dann in ihrem Konstruktor den Konstruktor der Elternklasse rufen, damit deren Attribute korrekt initialisiert und angelegt werden. Dies geschieht über das Konstrukt super().__init__(...).
Man sieht im Beispiel, dass die Kindklasse Hund ein weiteres Attribut name einführt und eine Methode beissen() hinzufügt. Das Attribut beine erbt sie von der Elternklasse.
h = Hund("Hasso")
print(h.beine) # -> 4
print(h.name) # -> Hasso
h.beissen() # -> Hasso hat dich gebissen
print("Hund")
h = Hund('Hasso')
h.beissen()
print(h.beine)
print("-------")
s = Tier(8)
print("Spinne")
print(s.beine)
Hund
Hasso hat dich gebissen
4
-------
Spinne
8
Standardmethoden
Jede Klasse in Python erbt automatisch eine Reihe von Standardmethoden. Diese Methoden können überschrieben werden (siehe unten), um das Verhalten an die jeweilige Klasse anzupassen.
Python hat die Konvention, dass Methoden, die von magisch von Python eingeführt werden mit zwei Underscores (__) beginnen und enden. Als Programmierer sollte man auf keinen Fall neue Methoden einführen, die diese Konvention benutzen.
Jede Klasse erbt eine Reihe von Standardmethoden
__init__: Konstruktor__repr__: String-Repräsentation des Objekts (für die Maschine)__str__: String-Repräsentation des Objekts (für Menschen)__eq__: Vergleicht den Inhalt (für==-Operator)- …
Die __init__-Methode haben wir im Zusammenhang mit dem Konstruktor bereits kennen gelernt.
Für die Umwandlung eines Objektes in einen String gibt es in Python zwei verschiedene Methoden: __repr__ und __str__. Der Unterschied besteht in der Zielgruppe der Methode:
__str__wendet sich an den Benutzer uns gibt den Inhalt des Objektes in einer Form aus, die für einen Menschen gut verständlich ist. Wie diese Format genau aussieht, kann die Entwicklerin selbst entscheiden. Diese Methode wird gerufen, wenn man das Objekt anstr()übergibt.__repr__liefert eine String-Repräsentation des Objektes, die für eine Maschine verständlich ist. Was ist damit gemeint? Die Ausgabe von__repr__sollte, wenn man sie dem Python-Interpreter verfüttert, wieder das Objekt erzeugen können. D. h. sie liefert einen gültigen Python-Ausdruck als String, der das Objekt erzeugen kann. Diese Methode wird gerufen, wenn manrepr()mit dem Objekt aufruft.
class Auto:
def __init__(self, name, vmax):
self.name = name
self.vmax = vmax
def __str__(self):
return f"{self.name}: vmax={self.vmax}"
def __repr__(self):
return f"Auto('{self.name}', {self.vmax})"
p = Auto("Porsche 911", 427)
print(str(p)) # -> Porsche 911: vmax=427
print(repr(p)) # -> Auto('Porsche 911', 427)
# Die Ausgabe von repr(...) kann man an eval() übergeben
# und bekommt wieder das Objekt heraus
p2 = eval(repr(p))
print(p2) # -> Porsche 911: vmax=427
Die Methode eval kann einen beliebigen String als Python-Code interpretieren. Hierbei ist zu beachten, dass
- auf keinen Fall Strings von außen in den Code einfließen sollten
- die Ausführung deutlich langsamer ist
- der Code nur sehr schwer zu verstehen ist.
Besonders Punkt 1 kann sehr viel Kopfzerbrechen verursachen: Jede Möglichkeit Daten, die von außen kommen, als Code zu interpretieren, öffnet Tür und Tor für sogenannte Code Execution oder noch schlimmer Remote Code Execution Schwachstellen. Ein Angreifer kann dem Programm durch geeignete Eingaben seinen Schadcode unterschieben und diesen dann ausführen lassen.
Überschreiben
Die Methoden, die von der Elternklasse geerbt werden, passen nicht immer für die Kindklasse. Insbesondere die bereits erläuterten Methoden __str__ etc. müssen von der Kindklasse ersetzt werden können, weil die Standardimplementierung keinen Sinn ergibt.
- Nicht immer passen die Methoden der Elternklasse für die Kinder
- Subklassen können Methoden ihrer Eltern überschreiben (overwrite)
- Die neue Methode ersetzt die alte
class A:
def __str__(self):
return "Ich bin ein A!"
a = A()
print(a) # -> Ich bin ein A
Operatoren überladen
Klassen können die vorhandenen Operatoren von Python für ihre Zwecke anpassen, indem sie sie überladen. Wir haben das bereits bei den Listen gesehen, die + und * mit einer neuen Bedeutung überlegen.
Jede Klasse kann Operatoren für ihre Zwecke umdefinieren.
Operatoren überladen (operator overloading)
- Operatoren (
+,-,*,/) werden für eihene Klassen umdefiniert - Zu jedem Operator gehört eine Methode
__add__für+__sub__für-- …
Es gibt in Python eine eingebaute Unterstützung für komplexe Zahlen. Trotzdem werden diese im folgenden Beispiel genutzt, um das Überladen von Operatoren zu zeigen.
class Complex:
def __init__(self, r, i):
self.r = r
self.i = i
def __add__(self, o):
return Complex(self.r + o.r, self.i + o.i)
def __str__(self):
return "Complex({}, {})".format(self.r, self.i)
c1 = Complex(1, 2)
c2 = Complex(4, 7)
print(c1 + c2) # -> Complex(5, 9)