Properties statt Getters und Setters

Properties

Venezianische Masken

Getters (auch als 'Accessors' bekannt) und Setter (auch bekannt als 'Mutators') werden in vielen objektorientierten Programmiersprachen verwendet, um das Prinzip der Datenkapselung sicherzustellen. Die Datenkapselung erfolgt- wie wir in unserer Einführung auf Objektorientierte Programmierung unseres Tutorials erfahren haben - über eine Bündelung der Daten mit Methoden, die auf diesen arbeiten. Diese Methoden sind der Getter zum Abrufen der Daten und der Setter zum Ändern der Daten. Nach diesem Prinzip werden die Attribute einer Klasse privatisiert, um den Code zu verbergen und zu schützen.

Leider ist die Überzeugung weit verbreitet, dass eine richtige Python-Klasse private Attribute mithilfe von Gettern und Setzern kapseln sollte. Sobald diese Programmierenden ein neues Attribut einführen, machen sie es zu einer privaten Variablen und erstellt "automatisch" einen Getter und einen Setter für dieses Attribut. Solche Programmierenden können sogar einen Editor oder eine IDE verwenden, die automatisch Getter und Setter für alle privaten Attribute erstellt. Diese Tools warnen die Programmierenden sogar, wenn sie ein öffentliches Attribut verwenden wollen! Manche haben OOP in anderen Sprachen so gelernt, dass sie die Stirne runzeln oder entsetzt sein werden, wenn sie Folgendes lesen: Die pythonische Art, Attribute einzuführen, besteht darin, sie öffentlich zu machen.

Wir werden dies später erklären. Zunächst zeigen wir im folgenden Beispiel, wie wir eine Klasse auf Java-ähnliche Weise mit Get- und Setzern entwerfen können, um das private Attribut self.__ x:

zu kapseln
In [1]:
class P:
    def __init__(self, x):
        self.__x = x

    def get_x(self):
        return self.__x

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

Wir zeigen nun, wie man mit dieser Klasse arbeiten kann:

In [2]:
p1 = P(42)
p2 = P(4711)
print(p1.get_x())
42
In [3]:
p1.set_x(47)
p1.set_x(p1.get_x()+p2.get_x())
print(p1.get_x())
4758

Was halten Sie von dem Ausdruck "p1.set_x(p1.get_x()+p2.get_x())"? Das ist doch hässlich? Es wäre doch viel einfacher, einen Ausdruck wie den folgenden zu schreiben, wenn wir ein öffentliches Attribut x hätten:

p1.x = p1.x + p2.x

Eine solche Zuordnung ist leichter zu schreiben und vor allem leichter zu lesen als der Javaesque-Ausdruck.

Schreiben wir die Klasse P pythonisch um. Kein Getter, kein Setter und anstelle des privaten Attributs self .__ x verwenden wir ein öffentliches:

In [4]:
class P:

    def __init__(self,x):
        self.x = x

Schön, nicht wahr? Nur drei Codezeilen, wenn wir die leere Zeile nicht zählen!

In [5]:
p1 = P(42)
p2 = P(4711)
print(p1.x)
42
In [6]:
p1.x = 47
p1.x = p1.x + p2.x
print(p1.x)
4758

"Aber, aber, aber, aber, aber ...", wir können sie heulen und schreien hören, "aber wo bleibt die Datenverkapselung?" Ja, in diesem Fall erfolgt keine Datenkapselung. Wir brauchen es in diesem Fall nicht. Das einzige, was get_x und set_x in unserem Startbeispiel taten, war, "die Daten durchzureichen", ohne etwas Zusätzliches zu tun.

Aber was passiert, wenn wir die Implementierung in Zukunft ändern wollen? Dies ist ein ernstes Argument. Nehmen wir an, wir möchten die Implementierung folgendermaßen ändern: Das Attribut x kann Werte zwischen 0 und 1000 haben. Wenn ein Wert größer als 1000 zugewiesen wird, sollte x auf 1000 gesetzt werden. Entsprechend sollte x auf 0 gesetzt werden, wenn der Wert kleiner als 0 ist.

Es ist einfach, die Methoden unserer ersten P-Klasse anzupassen, um dieses Problem abzudecken. Wir ändern die set_x-Methode entsprechend:

In [7]:
class P:

    def __init__(self,x):
        self.set_x(x)

    def get_x(self):
        return self.__x

    def set_x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x

Wir können nun mit dieser Klasse arbeiten und erkennen, dass das Attribut automatisch angepasst wird:

In [8]:
p1 = P(9999)
print(p1.get_x())
1000
In [9]:
p2 = P(15)
print(p2.get_x())
15
In [10]:
p3 = P(-1)
print(p3.get_x())
0

Aber es gibt einen Haken an der Sache: Nehmen wir an, wir haben unsere Klasse mit einem öffentlichen Attribut und ohne Methoden entworfen:

In [11]:
class P2:

    def __init__(self,x):
        self.x = x

Viele Leute haben es bereits oft benutzt und sie haben Code wie diesen geschrieben:

In [12]:
p1 = P2(42)
p1.x = 1001
print(p1.x)
1001

Würden wir nun die Klasse P2 so wie P umschreiben, bedeutete dies, die Schnittstelle zu brechen. Das Attribut x ist nicht mehr verfügbar. Das ist der Grund, warum in vielen anderen Sprachen empfohlen wird, nur private Attribute mit Getter und Setter zu verwenden, damit man jederzeit die Implementierung ändern kann, ohne die Schnittstelle ändern zu müssen.

Python bietet jedoch eine Lösung für dieses Problem. Die Lösung heißt Properties!

Die Klasse mit Properties sieht folgendermaßen aus:

In [13]:
class P:

    def __init__(self, x):
        self.set_x(x)

    def get_x(self):
        return self.__x

    def set_x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x

    x = property(get_x, set_x)

Diese Klasse lässt sich nun so benutzen, als gäbe es ein öffentliches Attribut x. Es gibt aber keines!

In [14]:
p = P(122)

y = p.x
print(y)
122

Bei der Auswertung von p.x wurde von Python die Methode get_x aufgerufen, da das erste Argument von property immer eines Referenz auf den Getter ist, unabhängig vom Namen. Also der folgende Code ist äquivalent zum vorigen:

In [15]:
p = P(122)

y = p.get_x()
print(y)
122

Nun weisen wir dem Attribut einen neuen Wert zu:

In [23]:
p.x = 44

# Dies ist äquivalent zu 
p.set_x(44)

print(p.x)
44

In der neuesten Version gibt es jedoch noch ein Problem. Wir haben jetzt zwei Möglichkeiten, auf den Wert von x zuzugreifen oder ihn zu ändern: Entweder mit "p1.x = 42" oder mit "p1.set_x(42)". Auf diese Weise verletzen wir eine der Grundlagen von Python: "Es sollte einen - und vorzugsweise nur einen - offensichtlichen Weg geben, dies zu tun." (Siehe Zen of Python)

Wir können dieses Problem leicht beheben, indem wir die Getter- und die Setter-Methode in private Methoden umwandeln, auf die User unserer Klasse P nicht mehr zugreifen können:

In [24]:
class P:

    def __init__(self, x):
        self.__set_x(x)

    def __get_x(self):
        return self.__x

    def __set_x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x

    x = property(__get_x, __set_x)

Python wäre jedoch nicht Python, wenn es nicht noch einen eleganteren Weg gäbe. Dieser geht mit Dekorateuren.

Die Methode, die zum Abrufen eines Werts - also der Getter - verwendet wird, wird mit "@property" dekoriert, d.h. wir setzen diese Zeile direkt vor den Header der Funktion. Der Name der Funktion entspricht jetzt einfach dem Namen der Property, in unserem Fall also x. Die Methode, die als Setter fungieren muss, wird mit "@x.setter" dekoriert. Wenn die Property "foo" genannt worden wäre, müssten wir die Setter-Funktionalität mit "@foo.setter" dekorieren. Zwei Dinge sind bemerkenswert: Wir setzen einfach die Codezeile "self.x = x" in die __init__ -Methode und die Property-Methode x wird verwendet, um die Grenzen der Werte zu überprüfen. Das zweite interessante ist, dass wir "zwei" Methoden mit demselben Namen und einer unterschiedlichen Anzahl von Parametern "def x (self)" und "def x (self, x)" geschrieben haben. Wir haben in einem früheren Kapitel unseres Kurses gelernt, dass dies nicht möglich ist. Es funktioniert hier aufgrund der Dekoration:

In [25]:
class P:

    def __init__(self,x):
        self.x = x

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 1000:
            self.__x = 1000
        else:
            self.__x = x
In [26]:
p1 = P(9999)
print(p1.x)

p2 = P(522)
print(p2.x)

p3 = P(-1900)
print(p3.x)
1000
522
0
Roboter mit Herz und Gefühlen

Nach dem, was wir bisher geschrieben haben und was auch in anderen Büchern und Tutorials zu sehen ist, könnten wir leicht den Eindruck gewinnen, dass es eine Eins-zu-Eins-Verbindung zwischen Properties und den Attributen geben sollte, d.h. jedes Attribut hat oder sollte eine eigene Property haben und umgekehrt. Selbst in anderen objektorientierten Sprachen als Python ist es normalerweise keine gute Idee, eine solche Klasse zu implementieren. Der Hauptgrund ist, dass viele Attribute nur intern benötigt werden, und das Erstellen von Schnittstellen für den Benutzer der Klasse die Benutzerfreundlichkeit der Klasse unnötig verringert. Die möglichen User einer Klasse sollten nicht in unzähligen - hauptsächlich unnötigen - Methoden oder Properties "ertrinken"!

Das folgende Beispiel zeigt eine Klasse mit internen Attributen, auf die von außen nicht zugegriffen werden kann. Dies sind die privaten Attribute self.__potential_physical und self.__potential_psychic. Darüber hinaus zeigen wir, dass eine Property auch aus den Werten von mehr als einem Attribut abgeleitet werden kann. Die Property "condition" unseres folgenden Beispiels gibt den Zustand des Roboters in einer beschreibenden Zeichenfolge zurück. Der Zustand hängt von der Summe der Werte des psychischen und des physischen Zustands des Roboters ab:

In [1]:
class Robot:

    def __init__(self, name, build_year, lk = 0.5, lp = 0.5 ):
        self.name = name
        self.build_year = build_year
        self.__potential_physical = lk
        self.__potential_psychic = lp

    @property
    def condition(self):
        s = self.__potential_physical + self.__potential_psychic
        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!" 

    
x = Robot("Marvin", 1979, 0.2, 0.4 )
y = Robot("Caliban", 1993, -0.4, 0.3)
print(x.condition)
print(y.condition)
Es geht mir gut!
Fühle mich schlecht!

Öffentliche statt private Attribute

Fassen wir die Verwendung privater und öffentlicher Attribute, Getter und Setter sowie Properties zusammen: Nehmen wir an, wir entwerfen eine neue Klasse und denken über eine Instanz oder ein Attribut "OurAtt" nach, die wir für das Design unserer Klasse benötigen. Dazu müssen wir folgende Punkte beachten:

  • Wird der Wert von "OurAtt" von den möglichen Usern unserer Klasse wirklich benötigt?
  • Wenn nicht, können oder sollten wir es zu einem privaten Attribut machen.
  • Wenn darauf zugegriffen werden muss, machen wir es als öffentliches Attribut zugänglich
  • Wir definieren es als privates Attribut mit der korrespondierender Property, wenn und nur wenn wir einige Überprüfungen oder Transformationen der Daten durchführen müssen. (Als Beispiel können Sie sich noch einmal unsere Klasse P ansehen, in der das Attribut im Intervall zwischen 0 und 1000 liegen muss.)
  • Alternativ können Sie einen Getter und einen Setter verwenden, aber die Verwendung einer Property ist der pythonische Weg, um damit umzugehen!

Nehmen wir an, wir haben "OurAtt" als öffentliches Attribut definiert.

In [2]:
class OurClass:

    def __init__(self, a):
        self.OurAtt = a


x = OurClass(10)
print(x.OurAtt)
10

Unsere Klasse wird seit einiger Zeit erfolgreich von anderen Benutzern verwendet. Jetzt kommt der Punkt, der einige traditionelle OOPistas aufschreckt: Stellen Sie sich vor, "OurAtt" wurde als Ganzzahl verwendet. Jetzt muss unsere Klasse sicherstellen, dass "OurAtt" einen Wert zwischen 0 und 1000 haben muss? Ohne Property, also wenn Python diese Möglichkeit nicht kennen würde, wäre dies wirklich ein schreckliches Szenario! Aufgrund von Properties ist es jedoch einfach: Wir erstellen eine Property-Version von "OurAtt".

In [3]:
class OurClass:

    def __init__(self, a):
        self.OurAtt = a

    @property
    def OurAtt(self):
        return self.__OurAtt

    @OurAtt.setter
    def OurAtt(self, val):
        if val < 0:
            self.__OurAtt = 0
        elif val > 1000:
            self.__OurAtt = 1000
        else:
            self.__OurAtt = val


x = OurClass(10)
print(x.OurAtt)
10

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 sollte einen --- und bevorzugt genau einen --- offensichtlichen Weg geben, es zu tun.'')