Link Search Menu Expand Document

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.

Anlegen mehrerer Autos
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.

self zeigt auf das aktuelle Objekt
class Selfie:
    def __init__(self):
        print(self)

a = Selfie()
print(a)
Ausgabe
<__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.

Attribute und Konstruktor-Parameter
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:

Klasse mit self in Ruby
class Auto
  def methode()
    print(self)
  end
end

a = Auto.new()
a.methode() # -> #<Auto:0x0000559d8e6846e8>

In Python muss man den self-Parameter selbst angeben:

Klasse mit self in Python
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 self explizit 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)
Ausgabe
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 an str() ü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 man repr() mit dem Objekt aufruft.
Beispiel: __repr__ und __str__
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

  1. auf keinen Fall Strings von außen in den Code einfließen sollten
  2. die Ausführung deutlich langsamer ist
  3. 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)

Copyright © 2022 Thomas Smits