Properties als Ersatz für Getter und Setter

Properties

Venezianische Masken

In der folgenden Klasse P wollen wir zeigen, wie man korrekt im Sinne der Datenkapselung auf private Attribute zugreift. In den meisten Fällen muss man ein Attribut lesen und ändern können. Dazu muss man dann zwei Methoden schreiben: eine, um einen Attributwert lesen zu können (in unserem Fall ,,getX()''), und eine, um diesen Wert ändern zu können (in unserem Fall ,,setX()''). Diese Abfragemethoden werden allgemein auch als ,,Getter'' und die Änderungsmethoden als ,,Setter'' bezeichnet.

class P:
    def __init__(self,x):
        self.__x = x

    def getX(self):
        return self.__x

    def setX(self, x):
        self.__x = x

In der folgenden interaktiven Beispielsitzung zeigen wir, wie man mit diesen Methoden arbeitet:

>>> from p import P
>>> a = P(19)
>>> a.getX()
19
>>> a.setX(42)
>>> b = a.getX() * 0.56
>>> b
23.520000000000003
>>> 

Deutlich bequemer wäre es natürlich, wenn wir direkt
b = a.__x * 0.56 
statt
b = a.getX() * 0.56
schreiben könnten. Dies geht jedoch nicht, weil __x ein privates Attribut ist, und wir wollen ja keinesfalls das Konzept der Datenkapselung aufweichen oder verletzen. Mit den Properties bietet Python ein Sprachkonstrukt, was einem einen leichteren lesenden und schreibenden Zugriff auf private-Attribute ermöglicht.

Wir erweitern obiges Beispiel um eine Property x:

class P:
    def __init__(self,x):
        self.__x = x

    def getX(self):
        return self.__x

    def setX(self, x):
        self.__x = x

    x = property(getX, setX)

Das erste Argument von property muss dem ,,Getter'', also in unserem Fall der Methode getX(), entsprechen. Das zweite Argument enthält den Namen der ,,Setter''-Methode, also ,,setX()'' in unserem Beispiel. Jetzt können wir x benutzen, als wäre es eine öffentliche Variable. Für das folgende Beispiel gehen wir davon aus, dass das vorige Beispiel in p.py im aktuellen Arbeitsverzeichnis gespeichert wurde:

>>> from p import P
>>> a = P(19)
>>> b = P(10)
>>> c = a.x + b.x
>>> c
29
>>> 

Ohne Zweifel ist dies in der Benutzung angenehmer. Die Property x ,,fühlt sich an'' wie ein öffentliches Attribut, aber alle Zugriffe auf das private Attribut __x erfolgen dennoch nur über die korrekten Zugriffsmethoden, also setX() und getX().

Möchte man lediglich einen lesenden Zugriff auf ein privates Attribut erlauben, dann ruft man die Property nur mit einem Argument, also der ,,Getter''-Methode auf:

class P:
    def __init__(self,x):
        self.__x = x

    def getX(self):
        return self.__x

    def setX(self, x):
        self.__x = x

    x = property(getX)

Wir können in obiger Beispielanwendung sehen, dass wir lesend zugreifen können. Versuchen wir jedoch, der Property x etwas zuzuweisen, erhalten wir eine Fehlermeldung.

>>> from p import P
>>> a.x = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> 

Einen Schönheitsfehler hat unsere obige Implementierung noch. Wir sollten auf jeden Fall verhindern, dass die Methoden getX und setX direkt von außen benutzt werden können, damit wir nicht zwei Wege haben, um auf die Attribute zuzugreifen. Es ist ein Grundsatz von Python, dass es immer nur genau einen Weg geben soll.2
Wir definieren sie deshalb als private Methoden:

class P:
    def __init__(self,x):
        self.__x = x

    def __getX(self):
        return self.__x

    def __setX(self, x):
        self.__x = x

    x = property(__getX, __setX)


Roboter mit Herz und Gefühl Aus dem bisher Gesagten und aus den ansonsten in der Literatur und anderen Tutorials vorherrschenden Beispielen könnte man leicht den Eindruck gewinnen, dass es zwischen Properties und privaten Attributen immer eine Eins-zu-eins-Beziehung gibt. Also zu einer Property gibt es genau ein privates Attribut und umgekehrt.

Das dem nicht so ist, wollen wir im folgenden Beispiel zeigen. Wir definieren dazu zwei private Attribute self.__leistung_koerper, als Maß für das körperliche Wohlbefinden, und self.__leistung_psyche, als Maß für das psychische Wohlbefinden. Die Property "befinden" verknüpft dann diese beiden privaten Attribute unter Benutzung der privaten Methode __befinden.
class Roboter:

    def __init__(self, name, baujahr, lk = 0.5, lp = 0.5 ):
        self.name = name
        self.baujahr = baujahr
        self.__leistung_koerper = lk
        self.__leistung_psyche = lp

    def __befinden(self):
        s = self.__leistung_koerper + self.__leistung_psyche
        if s <= -1:
           return "Fühle mich miserabel!"
        elif s <= 0:
           return "Fühle mich schlecht!"
        elif s <= 0.5:
           return "Könnte besser sein!"
        elif s <= 1:
           return "Es geht mir gut!"
        else:
           return "Super!" 

    befinden = property(__befinden)
  
if __name__ == "__main__":
    x = Roboter("Marvin", 1979, 0.2, 0.4 )
    y = Roboter("Caliban", 1993, -0.4, 0.3)
    print(x.befinden)
    print(y.befinden)

Public-Attribute statt private Attribute

In den meisten objektorientierten Programmiersprachen ist es ein ungeschriebenes Gesetz, dass man möglichst alle Attribute als ,,private'' deklariert und für sie ,,Getter'' und ,,Setter'' schreibt. Häufig sieht es dann aber so aus, wie in unseren bisherigen primitiven Beispielen. Mit unserem Getter und Setter reichen wir den entsprechenden Wert nur durch, also zum Beispiel so:

class A:
    
    def __init__(self, val):
        self.__x = val
        
    def getx(self):
        return self.__x
    
    def setx(self,val):
        self.__x = val

Der ideale Python-Weg sieht wegen der Möglichkeiten, die sich durch die Properties ergeben, ganz anders aus. In Python definiert man diese Klasse zunächst einmal mit einem public-Attribut:

class A:
    
    def __init__(self, val):
        self.x = val

Wir können direkt auf dieses Attribut zugreifen, wie wir im folgenden sehen:

>>> a = A(10)
>>> print(a.x)
10
>>> a.x = 102
>>> print(a.x)
102
>>> 


Eine Horrorvorstellung für einen C++- oder einen Java-Programmierer. Wie sieht es aber aus, wenn man später doch Zugriffsfunktionen will, um bestimmte Tests oder Wandlungen in andere Datenformate vorzunehmen?

In Python ist dies ganz einfach: Wir machen aus dem public-Attribut x in der __init__-Methode eine private-Methode und erzeugen entsprechende Getter- und Setter-Funtionen, sowie eine Property x. Stellen wir uns im obigen Beispiel vor, dass wir sicherstellen wollen, dass die Werte sich nur zwischen 0 und 100 bewegen dürfen, d.h. wir gehen davon aus, dass numerische Werte übergeben werden!

Wir schreiben unsere Klasse entsprechend um:
class A:
    
    def __init__(self, val):
        self.__x = val

    def __getx(self):
        return self.__x
    
    def __setx(self,val):
        if val > 100:
            self.__x = 100
        else:
            self.__x = val
            
    x = property(__getx, __setx)

Wie wir sehen haben die Benutzer unsere Klasse immer noch die gleiche ,,Sichtweise'', d.h. das Inferface ist gleich geblieben. Für sie gibt es weiterhin ein ,,öffentliches'' Attribut x, obwohl es sich jetzt eigentlich um eine Property handelt:

>>> a = A(10)
>>> print(a.x)
10
>>> a.x = 102
>>> print(a.x)
100
>>> 


Fußnoten

1
Isaac Asimov formulierte in seiner Kurzgeschichte ,,Runaround'' (Astounding, 1942) die Robotergesetze. Sie werden deshalb nach ihm auch als ,,Asimowsche Gesetze'' bezeichnet.

2
Dies entspricht einem der 20 Grundsätze von Python, die Tim Peters in ,,The Zen of Python'' formuliert hatte: ,,There should be one-- and preferably only one --obvious way to do it.'' (Deutsch: ,,Es sollten einen --- und bevorzugt genau einen --- offensichtlichen Weg geben, es zu tun.'')