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

Eine Klasse besteht aus Variablen, die in diesem Zusammenhang Attribute genannt werden, und Funktionen, die man hier Methoden nennt.

In dem Abschnitt über Klassen haben wir die Klasse Kiste kennengelernt, die die Attribute breite, hoehe und tiefe hat.

Private und Public Member

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

Bei Python hat die Philosophie, dass Programmierer erwachsen sind und nicht vor sich selbst beschützt werden müssen. Das mag man zwar bei der Betrachtung des einen oder anderen Kollegen in Zweifel ziehen, führt aber oft zu sehr schlanken und effizienten Lösungen.

Die Konvention besagt, dass die Privatheit von Attributen durch vorangestellte Unterstriche notiert wird und sich doch bitte alle daran halten sollen. Beginnt der Bezeichner ...

Dann gibt es noch die Unterstriche am Ende. Diese Dinge habe ich mir nicht einfach ausgedacht, sondern man kann sie im offiziellen Style Guide von Python nachschlagen.

Privatisierung

Definieren wir die Kiste mit privaten Attributen, ergibt sich folgendes Bild:
class Kiste:
    def __init__(self, b, h, t):
        self.__breite = b
        self.__hoehe = h
        self.__tiefe = t
    def getVolumen(self):
        self.__vol = self.__breite*self.__hoehe*self.__tiefe
        return self.__vol

pk = Kiste(3, 5, 7)
Diese Konstruktion führt dazu, dass die Dimension der Kiste nur bei der Erzeugung des Objekts festgelegt werden kann. Anschließend ist sie nicht mehr änderbar. So etwas kann durchaus gewollt sein.

Sollen die Dimensionen auch nachträglich geändert werden können, muss eine entsprechende Methode eingebaut werden. Nach Konvention verwendet man dazu eine Methode, die nach dem Attribut benannt ist, allerdings mit einem set davor, beispielsweise setBreite für das Attribut breite.

class Kiste:
    def __init__(self, b, h, t):
        self.__breite = b
        self.__hoehe = h
        self.__tiefe = t
    def setBreite(self, breite):
        self.__breite = breite
    def setHoehe(self, hoehe):
        self.__hoehe = hoehe
    def setTiefe(self, tiefe):
        self.__tiefe = tiefe
    def getVolumen(self):
        self.__vol = self.__breite*self.__hoehe*self.__tiefe
        return self.__vol
Nun ergibt sich die Möglichkeit, die Berechnung des Volumens von getVolumen in die set-Funktionen auszulagern. getVolumen würde dann einfach nur die Varialbe _vol zurückgeben. So würde das Volumen nur dann neu berechnet, wenn wirklich ein Attribut geändert wird.

class Kiste:
    def __init__(self):
        self.__breite = 0
        self.__hoehe = 0
        self.__tiefe = 0
        self.__vol = 0
    def setBreite(self, breite):
        if self.__breite != breite:
            self.__breite = breite
            self.__vol = self.__breite*self.__hoehe*self.__tiefe
    ...
    def getVolumen(self):
        return self.__vol
Noch effizienter wird es, wenn sich das Objekt merkt, ob ein Parameter inzwischen geändert wurde und erst dann das Volumen berechnet wird, wenn es wirklich angefordert wird. Dazu könnte das Attribut für das Volumen bei jeder Dimensionsänderung auf den Wert -1 gesetzt werden. Berechnet wird nun wieder in der Methode getVolumen, aber nur noch dann, wenn das Volumen -1 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, wann berechnet wird und dass die Berechnung nicht unnötig ausgeführt wurde.
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 hinter property 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.

Die Funktionen setBreite() und getBreite() sind nicht so schön schlank wie der einfache Zugriff auf ein Attribut breite.

Python ermöglicht einen anscheinenden Zugriff auf die Attribute, der aber in Wirklichkeit auf Getter beziehungsweise Setter umgeleitet wird. Dazu wird unter dem Namen des Attributs eine property erstellt:

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.

Statische Klassen-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 oder Klassenvariable.

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
Die Variable anzahl kann als Klassenvariable nur über den Klassennamen angesprochen werden. Verwendet eine Methode einfach den Namen anzahl, so wird ein Attribut self.anzahl angesprochen. Will die Methode dagegen die Klassenvariable verändern, verwendet auch sie den Klassennamen, also Kiste.anzahl.
class Kiste:
    anzahl = 0
    ...
    def aendere(self):
        Kiste.anzahl = 100
        self.breite = 100

k = Kiste()
k.aendere()
Soll die Klassenvariable von außen zugegriffen werden, gelingt das nicht über ein Objekt, sondern ebenfalls über den Klassennamen.

Man kann auch Klassenmethoden definieren. Diese unterscheiden sich am auffälligsten von den Objektmethoden dadurch, dass sie keinen Parameter self haben. Da ihnen der Objektbezug durch self fehlt, können sie nicht auf Objektattribute zugreifen, aber auf Klassenattribute.

Der Aufruf solch einer Klassenmethode ist ebenfalls nur über den Klassennamen und nicht über das Objekt möglich, weil ansonsten der Interpreter den Parameter self vermisst.

class Kiste:
    anzahl = 0
    ...
    def aendereKlassVar():
        Kiste.anzahl = 3

k = Kiste()
Kiste.aendereKlassVar()
#k.aendereKlassVar() # Zugriff verboten
Der Aufruf der Klassenmethode über ein Objekt ist nur erlaubt, wenn die Methode als staticmethod explizit deklariert wird. Ansonsten vermisst der Interpreter den Parameter self.
class Kiste:
    anzahl = 0
    ...
    def aendereKlassVar():
        Kiste.anzahl = 3

k = Kiste()
Kiste.aendereKlassVar()
#k.aendereKlassVar() # Zugriff verboten
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 aendereKlassVar():
        Kiste.anzahl = 3
    aendereKlassVar = staticmethod(aendereKlassVar)

k = Kiste()
Kiste.aendereKlassVar()
k.aendereKlassVar() # Zugriff erlaubt