Der Python-Kurs: Klassen
Willemers Informatik-Ecke
Funktionen Python-Kurs Attribute

Mit den Klassen ermöglicht Python das objektorientierte Programmieren.

Eine Klasse definiert einen Datentyp, der aus mehreren Daten-Bestandteilen bestehen kann. So besteht ein Datum aus Tag, Monat und Jahr und ein Bruch aus Zähler und Nenner. Das müssen keine ganzzahligen Werte sein, sondern durchaus auch Strings oder komplexere Typen, ja sogar weitere Klassen.

Zu einem Datentyp gehört aber auch oft eine spezielle Funktionalität. So ist das Addieren zweier ganzer Zahlen anders umzusetzen als das Addieren von Brüchen oder gar von einem Datum mit einer ganzen Zahl. Aus diesem Grund definiert eine Klasse auch Funktionalitäten.

Damit gehören zu einem Objekt sowohl die Datenbestandteile, die man auch als Attribut bezeichnet, als auch die Funktionen, die auf das Objekt angewandt werden können. Diese Funktionen, die auf Objekte angewandt werden, nennt man Methoden.

Eine Klasse ist die Definition eines Objekts. Sie beschreibt also dessen Attribute und Methoden. Ein Objekt ist die Instanz einer Klasse oder, weniger hochtrabend ausgedrückt, die Variable vom Typ einer Klasse.

Eine selbstgebastelte Kiste

Als Beispiel definieren wir eine Klasse für eine Kiste. Sie hat drei Werte, nämlich Höhe, Breite und Tiefe. Darüber hinaus gibt es aber auch die Funktion getVolumen(), die nur im Zusammenhang mit einem passenden Objekt sinnvoll ist. Darum gehört sie in die Klassendefinition.
class Kiste:
    breite = 0
    hoehe  = 0
    tiefe  = 0
    def getVolumen(self):
        vol = self.breite*self.hoehe*self.tiefe
        return vol
Die Klasse entspricht dem Bauplan des Objekts. Das Programm wird mit Objekten arbeiten, weil diese den Variablen entsprechen.

Um ein Objekt anzulegen, wird es mit dem Klassennamen gefolgt von einem Klammerpaar initialisiert. Hier wird ein Objekt meinekiste der Klasse Kiste angelegt.

Anschließend kann auf die Attribute und die Methoden des Objekts über den Objektnamen zugegriffen werden. Zur Trennung wird ein Punkt dazwischengestellt.

meinekiste = Kiste()
meinekiste.breite = 1
meinekiste.hoehe = 2
meinekiste.tiefe = 3
print(meinekiste.getVolumen()) # gibt 6 aus

Konstruktoren

Der Konstruktor einer Klasse ist eine Funktion, die automatisch aufgerufen wird, sobald ein Objekt der Klasse erzeugt wird. Der Konstruktor kann so die Initialisierung des Objekts vornehmen. Bei Python heißt der Konstruktor einer Klasse immer __init__() (mit je zwei Unterstrichen vor und nach dem init).
class Kiste:
    def __init__(self):
        self.breite = 0
        self.hoehe = 0
        self.tiefe = 0
    def getVolumen(self):
        vol = self.breite*self.hoehe*self.tiefe
        return vol
Die Initialisierung der Member-Variablen wird nun in den Konstruktor überführt. Der Konstruktor ist verantwortlich für die Initialisierung eines Objekts und darum sollten alle Attribute hier initialisiert werden.

Der Selbstbezug self

Von einer Klasse werden Objekte erzeugt. Um auf die Variablen des Objekts zuzugreifen, wird self verwendet. Darum erhalten die Methoden auch als ersten Standardparameter immer dieses self. Der Konstruktor wird durch die Erzeugung eines Objekts, also hier durch Kiste() aufgerufen.

Sie können auch Konstruktoren mit Parametern definieren. Dann können Sie gleich bei der Erzeugung bereits Werte übergeben, mit denen ein Objekt initialisiert werden soll.

Konstruktoren können auch weitere Parameter erhalten, die zur Initialisierung von Member-Variablen verwendet werden können.

class Kiste:
    def __init__(self, b, h, t):
        self.breite = b
        self.hoehe = h
        self.tiefe = t
    def getVolumen(self):
        vol = self.breite*self.hoehe*self.tiefe
        return vol
Bei diesem Konstruktor muss zwischen den Klammern von Kiste drei Werte angegeben werden. Ansonsten meldet Python einen Fehler.
k = Kiste(2,3,4)
print(k.getVolumen()) # gibt 24 aus
Wie bereits bei Funktionen beschrieben, können Sie den Parametern auch Vorgabewerte geben. Der Konruktor verhält sich also wie eine Funktion.

Destruktor

Einen Destruktor, wie er von C++ bekannt ist, gibt es bei Python nicht. Sie können eine Methode mit dem Namen __del__() überschreiben. Diese Methode wird aufgerufen, wenn mit dem Befehl del die letzte Referenz auf dieses Objekt gelöscht wird. Im Gegensatz zu einer Compilersprache wie C++ ist bei einer bei einer Interpretersprache mit Garbage Collection nicht sicher, wann und ob dies auftritt.

Objekte und Referenzen

Eine Klasse beschreibt die Objekte, ist also quasi der Bauplan. Von dem Typ einer Klasse können Objekte, also Variablen erstellt werden, die dann tatsächlich Speicher in Anspruch nehmen und sich voneinander unterscheiden, außer natürlich in der Struktur, die ja die Klasse vorgibt.

Als Beispiel verwenden wir die Klasse Auto. Unser vereinfachtes Auto hat einen Modellnamen und eine Leistung, gemessen in PS. Methoden ersparen wird uns erst einmal.

class Auto:
    name = ""
    ps = 0
In Python ist es allerdings üblich, die Attribute im Konstruktor zu initialisieren und die Attribute nicht noch einmal aufzuzählen, weil Variablen bei Python bei ihrer ersten Zuweisung entstehen.

class Auto:
    def __init__(self):
        self.name = ""
        self.ps = 0
Und wenn wir schon einen Konstruktor haben, dann soll er auch das Auto gleich mit den notwenigen Daten füttern.
class Auto:
    def __init__(self, name, ps):
        self.name = name
        self.ps = ps
Nun definieren wir einen Renault R4 als Variable r4.
r4 = Auto("R4", 34)
Auf ähnliche Weise könnte ein Objekt a4 entstehen. Der hat allerdings 102 PS und heißt natürlich anders.
a4 = Auto("A4", 102)
Wir testen noch einmal die Inhalte:
print(r4.name)  # zeigt R4
print(r4.ps)    # zeigt 34
print(a4.name)  # zeigt A4
print(a4.ps)    # zeigt 102
Nun könnte man auch auf die Idee kommen, den A4 als Kopie des R4 zu erstellen und anschließend Namen und PS zu ändern. Allerdings entsteht bei einer Zuweisung eines Objekts nicht etwa ein neues Objekt, sondern eine Referenz auf das Objekt. Das bedeutet, dass die Variablen a4 und r4 auf denselben Speicher zugreifen. Das folgende Beispiel zeigt, dass durch Änderung über a4 auch der Inhalt von r4 verändert wird.
a4 = r4
a4.ps = 102
print(r4.ps)

Wozu Referenzen gut sind

Das Ganze sieht nach einem Fehler im Design aus. Warum erzeugt eine Zuweisung nicht ein neues Objekt, sondern eine nichtsnutzige Referenz? Weil Referenzen eben doch nicht unnütz sind. Beispielsweise können wir damit einen Stau erzeugen. Dazu ergänzen wir die Klasse Auto um ein Attribut next, das auf das nächste Auto in der Schlange verweisen soll.
class Auto:
    def __init__(self, name, ps):
        self.name = name
        self.ps = ps
        self.next = None
Die Variable next soll als Referenz dienen. Solange sie nicht belegt wird, also noch nirgendwohin zeigt, wird sie mit None belegt.

Nun erstellen wir einen R4, dann einen A4 und hängen den A4 hinten an den R4 an.

r4 = Auto("R4", 34)
a4 = Auto("A4", 102)
r4.next = a4
print(r4.next.name) # zeigt A4
print(r4.next.ps)   # zeigt 102
Mit solchen Referenzen kann man alle möglichen Strukturen basteln. Als Beispiel sollten Sie sich den Binärbaum ansehen.

Referenz auf das gleiche Element? is

Der Befehl is kann feststellen, ob zwei Variablen eine Referenz auf das gleiche Objekt sind. Wenn Sie folgende zwei Zeilen zum vorigen Skript ergänzen, erhalten Sie nacheinander die Ausgabe True und False.
print(r4.next is a4) # gibt True aus
print(r4 is a4) # gibt False aus

Gar keine Referenz: None

Soll eine Referenz explizit auf nichts verweisen, kann man ihr den Wert None zuweisen. Eine Abfrage auf eine Referenz, die None enthält, liefert False, jede andere liefert True.
if a4.next:
   print(a4.next.name)
else:
   print("Ende der Schlange")

Eine Kopie erstellen

Wenn es wirklich eine Kopie sein soll, hilft die Funktion copy() aus dem Modul copy, das aber zunächst importiert werden muss.
from copy import copy

a5 = copy(a4)
a5.ps=120
print(a4.ps) # zeigt 102

Ein eigener Datencontainer: Der Binärbaum

Ein Binärbaum ist eine dynamische Datenstruktur, in der Daten sortiert abgelegt werden. Durch einfache Kleiner/Größer-Abfragen kann man in einem idealen Baum von 1000 Elementen nach 10 Fragen das gesuchte finden, da 2**10 1024 ergibt.

Ein binärer Baum enthält Knoten, die mit je einer Referenz auf das nächstkleinere und das nächstgrößere Element verweisen. Kommt ein neues Element hinzu, wird zunächst die Stelle gesucht, wo das neue Element hingehört und dort ein Knoten eingefügt. Für das Auslesen eines Baumes werden rekursive Funktionen . eingesetzt, also Funktionen, die sich selbst aufrufen.

Die Klasse Baum stellt auf den ersten Blick nicht einen kompletten Baum dar, sondern nur einen Knoten. Dieser ist eher ein kleiner Ast, der einen Dateninhalt, einen Arm nach links und nach rechts besitzt. Werden neue Knoten an die Arme angefügt, entsteht ein Geflecht, das leichter als Baum identifiziert werden kann.

Rekursives Durchlaufen

Um den rekursiven Umgang mit einem Baum zu verstehen, ist ein Blick auf die Funktion anzeigen() angebracht. Die Funktion schaut zunächst auf den linken Ast. Hängt daran ein Knoten, ruft sie sich selbst auf und wird so irgendwann an das linke äußere Ende des Baums gelangen. Dann wird der Ausgabeaufruf print() den Inhalt anzeigen. Im nächsten Schritt wird der rechte Ast geprüft. Ist dort nichts, endet die Funktion und wird eine Stufe zurück zum letzten Selbstaufruf springen und dort mit der Funktion print() fortfahren. Findet die Funktion allerdings einen rechten Ast, dann wird sie wiederum sich selbst aufrufen und von dem Folgeknoten aus zunächst so weit wie möglich nach links absteigen.

Ein leerer Baum wird daran erkannt, dass der erste Knoten noch keinen Inhalt hat. Dies muss beim Bearbeiten des Baums also immer noch extra geprüft werden, bevor der rekursive Abstieg beginnt.

#!/usr/bin/python

class Baum:
    def __init__(self):
        self.__links = None
        self.__rechts = None
        self.__inhalt = None

    def einhaengen(self, inhalt):
        if self.__inhalt: # Baum ist nicht leer
            if self.__inhalt > inhalt: # sortiertes Einhaengen
                if self.__links:
                    self.__links.einhaengen(inhalt)
                else:
                    self.__links = Baum()
                    self.__links.__inhalt = inhalt
            else:
                if self.__rechts:
                    self.__rechts.einhaengen(inhalt)
                else:
                    self.__rechts = Baum()
                    self.__rechts.__inhalt = inhalt
        else: # Der Baum ist leer
            self.__inhalt = inhalt

    def anzeigen(self):
        if self.__links:   # links absteigen
            self.__links.anzeigen()
        if self.__inhalt:  # Sicherheitstest: Baum leer?
            print(self.__inhalt) # Aktion: Anzeigen
        if self.__rechts:  # rechts absteigen
            self.__rechts.anzeigen()

baum = Baum()
for i in "Bier", "Auto", "Schiff", "Wolke", "Pirat":
    baum.einhaengen(i)
baum.anzeigen()
Im Hauptprogramm wird der Baum angelegt und dann in einer Schleife mit den beliebtesten Passwörter der Deutschen belegt. Zum Test wird zuletzt der Inhalt des Baums ausgelesen und tatsächlich: Das Ergebnis ist sortiert.

Einbau einer Besucherfunktion

Ein Baum soll nicht immer nur angezeigt werden. Dazu muss der print()-Aufruf in der Funktion anzeigen() einfach nur durch einen anderen Befehl ersetzt werden.

Ein flexibler Ansatz besteht darin, der Funktion anzeigen() eine Funktion in den Parametern durchzureichen, die sie dann anstelle des print-Aufrufs ausführt. Da die Funktion dann nicht mehr anzeigt, nennen wir sie in tuwas() um.

class Baum:
    # ...
    def tuwas(self, funktion):
        if self.__links:   # links absteigen
            self.__links.anzeigen()
        if self.__inhalt:  # Sicherheitstest: Baum leer?
            funktion(self.__inhalt) # Aktion: funktion
        if self.__rechts:  # rechts absteigen
            self.__rechts.anzeigen()

def zeigmal(a):
    print(a)

# ...
baum.tuwas(zeigmal)
Beim Aufruf wird der Name der Funktion als Parameter übergeben und schließlich innerhalb der Funktion tuwas() aufgerufen. Hier wird am Beispiel die Funktion{zeigmal()} übergeben, die nun auch nur eine Ausgabe herbeiführt. Sie können aber jederzeit die Funktion ändern oder andere Funktionen verwenden.

Übungsaufgaben

Autokennzeichen

Ergänzen Sie die Klasse Auto um ein Autokennzeichen.
  1. Sorgen Sie dafür, dass es bei der Erstellung des Autos eingegeben werden muss.
  2. Ändern Sie die Klasse so, dass ein Kennzeichen angegeben werden kann, aber nicht muss. Wird bei der Erstellung des Autos kein Kennzeichen angegeben, soll "A-BC 123" verwendet werden.

Die Autovermietung

Ein Start-Up-Unternehmen vermietet Autos. Leider haben sie statt einer Garage nur einen nicht vollendeten Tunnel, in dem sie die Autos abstellen können. Das bedeutet, dass die Autos nacheinander in den Tunnel gefahren werden, aber immer nur der zuletzt hineingefahrene Wagen vermietet werden kann.

Erstellen Sie eine Klasse Tunnel, das Autos aufnimmt und diese in umgekehrter Weise wieder herausgibt.