Python Klassen

Willemers Informatik-Ecke


Bestellen bei Amazon
2015-07-31

Diese Seiten sind Grundlage meines Python-Buchs aus den ersten Recherchen.

Klassen können zwei oder mehr Variablen zusammenfassen, die eng zusammengehören. Beispielsweise kann eine Klasse Adresse aus Namen, Straße, Postleitzahl und Ort bestehen oder eine Klasse Auto Marke, Modell, Leistung und Geschwindigkeit enthalten.

Im Grunde erzeugen Sie mit einer Klasse die Schablone für einen eigenen Datentyp.

So wie ganze Zahlen andere Funktionen und Operatoren haben als Zeichenketten, sind auch für selbstgeschaffene Datentypen die passenden Funktionen normalerweise maßgeschneidert. Aus diesem Grund werden auch die Funktionen in die Klassendefinitionen aufgenommen. Klasse sind also die Zusammenführung aus Daten und Funktionen, die im objektorientierten Sprachgebrauch auch als Attribute und Methoden bezeichnet werden.

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__().
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.

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.

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.

Private Member

In der Software-Entwicklung gilt das Geheimnisprinzip. Alles, was nicht unbedingt öffentlich sein muss, sollte vor anderen Teilen des Programms verborgen sein.

Dagegen steht das Prinzip von Python, dass Programmierer erwachsen sind und wissen, was sie tun. Python verbietet also nicht den Zugriff auf private Strukturen, indem der Interpreter aufheult, sondern durch die Konvention, dass man einen Unterstrich vor die privaten Attribute und Methoden setzt. Einen Mechanismus, der dies prüft, bietet Python nicht.

Die scheinbare Selbstbehinderung der Privatisierung führt bei unserer Kiste zu der Möglichkeit, zu kontrollieren, ob sich die Kiste in ihrer Größe verändert. Dazu definieren wir um:

class Kiste:
    def __init__(self):
        self._breite = 0
        self._hoehe = 0
        self._tiefe = 0
    def setBreite(self, breite):
        self._breite = breite
    def setHoehe(self, hoehe):
        self._hoehe = hoehe
    def setTiefe(self, tiefe):
        self._tiefe = tiefe
Nun kann die Volumenberechnung aufgeteilt werden.
class Kiste:
    ...
    _vol = 0
    def getVolumen(self):
        self._vol = self._breite*self._hoehe*self._tiefe
        return self._vol
Der Aufrufer muss nun die Kistendimensionen per set-Methode aufrufen.
kiste = Kiste()
kiste.setBreite(2)
kiste.setHoehe(3)
kiste.setTiefe(4)
print(kiste.getVolumen())
Nun ergeben sich neue Möglichkeiten. Der Aufruf zur Berechnung des Volumens könnte beispielsweise von getVolumen in die setFunktionen ausgelagert werden. So würde nur dann neu berechnet, wenn wirklich ein Parameter geändert wird.

Noch effizienter wird es, wenn sich das Objekt merkt, ob ein Parameter inzwischen geändert wurde. Dazu könnte beispielsweise das Volumen auf den unmöglichen Wert -1 gesetzt werden. Berechnet wird nun nur noch, sofern das Volumen negativ ist.

class Kiste:
    ...
    def __init__(self):
        self._vol = -1
    ...
    def setBreite(self, breite):
        if self._breite != breite:
            self._breite = breite
            self._vol = -1
    def setHoehe(self, hoehe):
        if self._hoehe != hoehe:
            self._hoehe = hoehe
            self._vol = -1
    def setTiefe(self, tiefe):
        if self._tiefe != tiefe:
            self._tiefe = tiefe
            self._vol = -1
    def getVolumen(self):
        if (self._vol == -1):
            print("calc")
            self._vol = self._breite*self._hoehe*self._tiefe
        return self._vol
Zur Kontrolle meldet die Berechnungsmethode ihre Tätigkeit auf dem Bildschirm. So kann man erkennen, dass sie nicht unnötig aufgerufen wurde. Nun kann man beobachten, wann berechnet wird.
kiste = Kiste()
kiste.setBreite(2)
kiste.setHoehe(3)
kiste.setTiefe(4)
print(kiste.getVolumen())
print(kiste.getVolumen())
kiste.setHoehe(3)
print(kiste.getVolumen())
kiste.setBreite(1)
kiste.setTiefe(2)
print(kiste.getVolumen())
Setter und Getter verstecken
Der Zugriff auf die Attribute über Setter- und Getter-Funktionen sieht nicht besonders elegant aus, hat aber den Vorteil, dass das Programm die Kontrolle behält, wenn das Attribut verändert wird.
class EindimensionaleKiste:
    def __init__(self):
        self._breite = 0
    def setBreite(self, breite):
        # ...
    def getBreite(self):
        # ...
Die Funktionen setBreite() und getBreite() sind nicht so schön schlank wie der einfache Zugriff auf ein Attribut breite. Außerdem kennt man von anderen Sprachen die vielen Setter und Getter um alle Attribute, nur für den Fall, dass es irgendwann notwendig sein könnte, die Kontrolle zu erlangen.

Python macht dies grundlegend anders. Man greift auf die Attribute zu. Müssen dann doch einzelne unter die Obhut von Settern und Gettern gestellt werden, dann setzt man einen Unterstrich vor das Attribut, schreibt Getter und Setter-Methode und setzt dann eine Property, die dafür sorgt, dass der alte Attribut-Namen die Zugriffe auf die Setter und Getter umlenkt.

Nun wird die Property gesetzt. Als erstes Argument erhält sie den Namen der Get-Funktion. Der zweite ist der der Set-Funktion. Der Rückgabewert wird als Name für die Pseudovariable verwendet.

class EindimensionaleKiste:
    def __init__(self):
        self._breite = 0
    def setBreite(self, breite):
        # ...
    def getBreite(self):
        # ...
    breite = property(getBreite, setBreite)

kiste = EindimensionaleKiste()
kiste.breite = 5
print(kiste.breite)
Python erkennt, dass beim ersten Zugriff die Variable breite links von der Zuweisung steht, und ruft darum die Funktion setBreite() auf. Beim zweiten Zugriff wird breite ausgewertet und darum wird automatisch die passende get-Funktion aufgerufen.

Das hat zur Konsequenz, dass ein Python-Programmierer zunächst alle Attribute ohne Setter und Getter verwenden wird und diese erst bei Notwendigkeit über die Property einsetzen wird. Der Code des restlichen Programms muss nicht geändert werden.

Statische Variablen

Eine Variable innerhalb einer Klassendefinition bezieht sich normalerweise auf das Objekt, das von der Klasse gebildet wird. Das ist naheliegend, weil jede Kiste ihre eigene Breite hat oder zumindest haben kann.

Wenn wir aber wissen wollen, wie viele Kisten es gibt, dann müsste man einen solchen Zähler an die Klasse binden. Eine solche Variable, die es nur einmal für die gesamte Klasse gibt, bezeichnet man als statische Variable.

Statische Variablen werden durch den Zugriff über den Namen der Klasse erreicht. Entsprechend würde man also auf Kiste.anzahl zugreifen. Das folgende Listing zeigt, wie die erstellten Kisten gezählt werden:

class Kiste:
    anzahl = 0
    def __init__(self):
        self.breite = 0
        self.hoehe = 0
        self.tiefe = 0
        Kiste.anzahl += 1
Da die Variable anzahl als Klassenvariable existiert, kann auch über das Objekt auf eine Variable anzahl zugegriffen werden. Allerdings kann auf diesem Weg der Wert der statischen Variablen nicht verändert werden.
kiste = Kiste()
kasten = Kiste()
kiste.anzahl = 5
print("---", Kiste.anzahl)
print(kiste.anzahl)
Statische Funktionen können auf statische Variablen zugreifen, aber nicht auf normale Member-Attribute, da sie zu keinem Objekt gehören. Sie müssen mit der Deklaration als staticmethod angemeldet werden.
class Kiste:
    anzahl = 0
    def zeigeAnzahl(): 
        print("Die Instanzanzahl ist", Kiste.anzahl)
 
    zeigeAnzahl = staticmethod(zeigeAnzahl)

Wenn eine Zuweisung zur Referenz führt

Eine Zuweisung einer Zahl oder einer Zeichenkette kopiert den Wert in die zugewiesene Variable. Bei komplexeren Datenstrukturen wie Klassen wird bei der Zuweisung allerdings eine Referenz auf das bestehende Objekt erzeugt. Dadurch verweisen beide Variablen auf dasselbe Objekt.

Diese Vorgehensweise ist sehr flink und in den meisten Anwendungsfällen völlig ausreichend. Der Programmierer muss sich lediglich bewusst sein, dass es eine Referenz und keine Kopie ist. Änderungen über die neue Referenzvariable ändern auch den Inhalt der ersten Variablen.

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 *

class Klasse:
    wert = 0

eins = Klasse()
eins.wert = 5
zwei = eins        # referenziert
zwei.wert = 3
print(eins.wert)   # gibt 3 aus, nicht 5
drei = copy(eins); # kopiert
drei.wert = 7
print(eins.wert)   # gibt 3 aus
print(drei.wert)   # gibt 7 aus
Enthält das referenzierte Objekt selbst wiederum eine Referenz, was durchaus vorkommen kann, müsste man die Funktion deepcopy() verwenden, wollte man auch die tiefer liegenden Referenzen komplett kopieren.
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(eins is zwei) # gibt True aus
print(eins is drei) # 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.
RefLotto = None

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.

Selbstgebaute Operatoren durch Magic Members

Magic Members werden Funktionen genannt, die Operatoren realisieren. Sie können also für Ihre eigenen Klassen Operatoren wie das Pluszeichen definieren. Man spricht hier vom Überladen.

Die Magic Members beginnen mit zwei Unterstrichen und enden auch damit, also ganz ähnlich wie der Konstruktor und der Destruktor.

Zu den Magic Members gehört beispielsweise die Funktion __eq__(), die aufgerufen wird, wenn die Gleichheit zweier Objekte festgestellt werden soll.

class Kiste:
    def __eq__(self, kiste):
       return kiste.getVolumen()==self.getVolumen()
Auch für die anderen Vergleichsoperatoren gibt es passende Funktionen.

Operator Funktion
== __eq__()
!= __ne__()
<= __le__()
< __lt__()
>= __ge__()
> __gt__()
Auch mathematische Operatoren können so implementiert werden.
rator Funktion
+ __add__()
- __sub__()
* __mul__()
/ __div__()
Die Funktion __str__() ermöglicht es, ein Objekt der Klasse mit der Funktion str(objekt) in einen String zu verwandeln.

Erbschaftsangelegenheiten

Klassen können aufeinander aufbauen. Dazu wird eine Klasse als Erweiterung einer Basisklasse definiert. Dabei werden alle Attribute und Methoden der Basisklasse als eigene Attribute und Methoden übernommen, ohne dass der Programmierer das explizit benennen muss. Der Programmierer der neuen Klasse fügt nur noch spezielle Elemente der neuen Klasse hinzu oder überschreibt Methoden, die sich in der neuen Klasse anders verhalten sollen.

Wenn man eine Basisklasse zu einer neuen Klasse erweitert, spricht man davon, dass man eine Klasse von der Basisklasse ableitet oder auch, dass eine Klasse die Basisklasse beerbt.

Der Begriff Erweiterung trifft den Vorgang aber etwas präziser, weil es in erster Linie darum geht, dass die Basisklasse um zusätzliche Attribute oder Methoden ergänzt wird.

Vererbung am Beispiel

In einem Warenhaus gibt es vielfältigste Waren. Allen gemeinsam ist, dass man sie verkaufen will. Darum haben alle Waren einen Preis. Damit man sie unterscheiden kann, haben sie eine Artikelnummer und eine Bezeichnung.

Neben diesen Attributen haben Waren Methoden. Zur Preisbestimmung kann es die Funktion getVerkaufspreis() und für die Inventur die Funktion getZeitwert() geben. Natürlich will die Inventur-Routine alle Waren bewerten und keine Fallunterscheidung zwischen Lebensmitteln und Unterwäsche machen.

Ein wichtiger Warenbestandteil sind Lebensmittel. Sie haben die gleichen Eigenschaften wie alle Waren, aber zusätzlich ein Mindesthaltbarkeitsdatum. Der Teil der Software, der sich mit dem Lebensmittelbestand beschäftigt, wird alle Lebensmittel daraufhin prüfen, ob sie nahe am Verfallsdatum sind und dann ihren Preis reduzieren. Haben sie das Verfallsdatum erreicht, wird die Software das Aussortieren veranlassen.

Alle Bekleidungsartikel sind natürlich ebenfalls Waren mit all deren Eigenschaften. Sie haben aber auch eine Größe. Dafür haben sie kein Mindesthaltbarkeitsdatum. Im Gegenteil: Wenn man sie nur lang genug auf Lager hält, werden sie irgendwann wieder modern.

Nicht alle Unterschiede zwischen Objekten erzwingen eine Ableitung. So kann man Schuhe und Unterwäsche solange als Kleidung führen, so lange nicht neue Attribute oder grundlegend andere Arbeitsabläufe dies nahelegen. Die Tatsache, dass sich Unterwäsche und Schuhe in unterschiedlichen Lagern befindet, kann man durch ein Attribut lager abbilden, das aber dann eben sowohl Unterwäsche wie Schuhe enthalten. Dagegen wäre der Pfand bei Getränken ein Grund, die Getränke von den Lebensmitteln abzuleiten, da die anderen Lebensmittel eben keinen Pfand haben.

Das Beispiel im Quellcode
Ein kleines Beispielprogramm soll demonstrieren, wie eine Ableitung im Quellcode aussieht. Dazu wird eine Basisklasse Ware geschaffen. Die Klassen Lebensmittel und Kleidung werden davon abgeleitet. Dazu wird hinter dem Namen der Klasse in runden Klammern die Basisklasse angegeben.

Daraus ergibt sich ein Klassenbaum. Da Python für alle Klassen eine Basisklasse namens object stellt, sollte jede Klasse davon abgeleitet werden. Also erhält nun auch Ware eine Basisklasse, eben object. Bei den anderen Klassen muss object nicht mehr genannt werden, da sie diese Erbschaft mit den anderen Eigenschaften von Ware erben.

class Ware(object):
    def __init__(self):
        self._bezeichnung = ""
        self._preis = 0.0
        self._artikelnr = 0
    def getPreis(self):
        return self._preis

class Lebensmittel(Ware):
    def __init__(self):
        Ware.__init__(self) # Basisklasse initialisieren
        self._mindestHaltbarkeit = Datum()

class Kleidung(Ware):
    def __init__(self):
        Ware.__init__(self) # Basisklasse initialisieren
        self._groesse = 0

Polymorphie

Wenn in von einer Basisklasse eine neue Klasse abgeleitet wird und die neue Klasse eine Methode der Basisklasse überschreibt, dann gilt für ein Objekt der neuen Klasse die überschriebene Methode und bei der Basisklasse die der Basisklasse. Dieser Mechanismus wird in der objektorientierten Programmierung als Polymorphie bezeichnet.

Dieser Abschnitt wird Sie vermutlich nur betreffen, wenn Sie das Konzept der Polymorphie von anderen Programmiersprachen her kennen und wissen wollen, wie es in Python gelöst wird.

Streng typisierte Programmiersprachen müssen Vorbereitungen treffen, wenn bei der Übergabe von Objekten als Parameter an Funktionen, die Objekte der Basisklasse erwarten. Bei der Übergabe geht dann nämlich die Information verloren, dass es sich um eine abgeleitete Klasse handelt.

Python hat an dieser Stelle kein Problem, weil die Parametervariable ihren Typ durch die Initialisierung erhält und so automatisch die richtige Methode aufgerufen wird. Insofern könnte man sagen, dass Polymorphie keine spezielle Unterstützung durch den Programmierer benötigt.

Abstrakte Basisklassen

Auch in Python ist es möglich, abstrakte Basisklassen zu verwenden. Dabei implementiert nicht die Basisklasse den Code, sondern nur die abgeleitete.
from abc import ABCMeta, abstractmethod
class Abstrakt(metaclass=ABCMeta):
    @abstractmethod
    def tuwas(self):
        pass

class Ableit(Abstrakt):
    def tuwas(self):
        print("tuwas")

objekt = Ableit()
Ist die Methode tuwas() in Ableit nicht definiert, gibt es einen TypeError, weil sich abstrakte Klassen nicht instanziieren lassen.

Mehrfachvererbung

Python erlaubt Mehrfachvererbung, indem in der Klammer der erweiternden Klasse mehrere Klassennamen angegeben werden.
class Freiberufler(Angestellter, Unternehmer)
Hier erbt der Freiberufler sowohl Eigenschaften des Angestellten als auch die des Unternehmers.

Homepage (C) Copyright 2014, 2015 Arnold Willemer