OOP - Property

Properties

Bemerkung

In vielen objektorientierten Sprachen wie Java und C# besitzen private Objektattribute die von außen nicht direkt zugänglich sind. Es müssen oft Getter und Setter Methoden geschrieben werden um auf solche privaten Attribute zugreifen zu können. Statt Methoden stellen einige Programmiersprachen sogenannte Properties zur Verfügung.

In Python sind jedoch alle Attribute und Methoden öffentlich, daher ist es eigentlich nicht richtig von private, protected und public zu sprechen. Auch auf Attribute mit doppelten Unterstrich wie etwa __name kann zugegriffen werden.

Python ist jedoch eine Sprache mit viel Konventionen an die sich Entwickler halten.

private, protected und public ?

In einigen Büchern und Onlinequellen kann lesen

  • self.x sei public

  • self._x sei protected

  • self.__x sei private

Das ist aber nur eine Konvention an die sich Entwickler halten und keine Eigenschaft von Python. Auch die Unterscheidung von protected und private kann als falsch angesehen werden, da die meisten Entwickler einen einfachen Unterstrich für ihre „private“ Attribute verwenden.

Wir wollen uns diesen Sachverhalt nun ansehen. Dazu implementieren wir eine Klasse mit drei Variablen.

class A():
    def __init__(self):
        self.x = 1      # public ?
        self._x = 2     # protected ?
        self.__x = 3    # private ?

Die Variablen werden dabei in einem Dict gespeichert und können mit __dict__ angezeigt werden.

a = A()
print(a.__dict__)
{'x': 1, '_x': 2, '_A__x': 3}

Schon jetzt dürfte klar sein, dass es sich hier nicht um eine private Variable handelt. Es kann ja ganz einfach auf das Dict zugegriffen werden.

print(a.__dict__['_A__x'])
3

Um die Sache noch deutlicher zu machen werden wir nun alle Werte lesen und schreiben.

a = A()
print(a.x)
print(a._x)
print(a._A__x)
a.x = -1
a._x = -2
a._A__x = -3
print(a.x)
print(a._x)
print(a._A__x)
1
2
3
-1
-2
-3

Python besitz also kein Datenschutzmodel wie C++, Java oder C#. Das betrifft nicht nur Variable sondern auch Methoden.

class A():
    def __init__(self):
        self.x = 1
        self._x = 2 
        self.__x = 3
    
    def xprint(self):
        print(self.x)
        
    def _xprint(self):
        print(self._x)
    
    def __xprint(self):
        print(self.__x)
a = A()
a = A()
a.xprint()
a._xprint()
a._A__xprint()
1
2
3

Konvention

Oft ist es aber notwendig Werte auf einen Gültigkeitsbereich zu überprüfen. Dem Programmierer sollen Schnittstellen angeboten werden um Daten an ein Objekt zu übergeben. Sind die Überprüfungen und Manipulation abgeschlossen soll der gültige Wert in eine interne Variable gespeichert werden. Diese interne Variable wird dann für die Weiterverarbeitung verwendet.

class A():
    def __init__(self,x):
        self._x = None   # "private" attribute
        self.set_x(x)    # call setter method
        
    def get_x(self):
        return self._x
    
    def set_x(self,x):
        if (isinstance(x,int)):
            if x>=0 and x<=100:
                self._x = x
            else:
                raise ValueError("False Range")
        else:
            raise ValueError("x should be a <int>")
a = A(20)
a.set_x(50)
a.get_x()
50

Um die Variable zu lesen und zu schreiben sind aber jetzt 2 Funktionen get_x()und set_x() nötig. Das schein unnötig kompliziert. Um den Variablenzugriff zu vereinfachen können wir @property verwenden.

class A():
    def __init__(self,x):
        self._x = None
        self.x = x
    
    def get_x(self):
        return self._x
    
    def set_x(self,x):
        if (isinstance(x,int)):
            if x>=0 and x<=100:
                self._x = x
            else:
                raise ValueError("False Range")
        else:
            raise ValueError("x should be a <int>")
    
    x = property(get_x,set_x)
a = A(20)
a.x
20
a = A(20)
a.x = 50
a.x
50

Nun können wir sehr einfach auf Instanzattribute zugreifen. Dieses Programm ist zweifellos korrekt. Es gibt allerdings einen Weg der üblicher begangen wird.

a = A()
a.x
0
a = A(20)
a.x = 50
a.x
50

Dieser Code ist jetzt deutlich einfacher zu lesen

a = A(50)
a.x
50

Warnung

In vielen Tutorial sieht man folgenden Code:

class A():
    def __init__(self,x):
        self._x = x # "private" attribute initialized with x, it does not call the @x.setter

Dieser Code kann aber zu einem Fehler führen, da der @x.setter nicht aufgerufen wird.

Tipp

Wir müssen in dern __init__ Methode auf jeden Fall self.x = x hinzufügen, weil nur so der @x.setter auch ausgeführt und die Überprüfungen durchgeführt werden.

class A():
    def __init__(self,x):
        self._x = None # "private" attribute initialized with None, 
                       #  it does not call the @x.setter
            
        self.x = x # property and calls the @x.setter

Die Codezeile self._x = None ist nicht zwingen erforderlich, entspricht aber in vielen Projekten der Konvention und wird auch von machen IDE’s verlangt damit keine Warnungen erzeugt werden. Weiters ist der Code auch besser lesbar und durch @x.setter wird self._x in jedem Fall dem Objekt hinzugefügt.

Property mit Doppelten Unterstrich (__) ?

Warnung

In einigen Büchern und Tutorials wird vorgeschlagen _x durch __x zu ersetzen, weil dadurch angeblich ein „privates“ Attribute genutzt wird. Wie wir aber schon wissen handelt es sich bei __x nicht um private Attribute. Die meisten großen Python-Projekte nutzt einen Unterstrich _x um interne bzw „private“ Attribute zu kennzeichnen.

class A():
    def __init__(self,x=0):
        self.__x = None
        self.x = x
    
    @property
    def x(self):
        return self.__x
    
    @x.setter
    def x(self,x):
        if (isinstance(x,int)):
            if x>=0 and x<=100:
                self.__x = x
            else:
                raise ValueError("False Range")
        else:
            raise ValueError("x should be a <int>")
a = A()
a.__dict__
{'_A__x': 0}

Wie oben schon besprochen werden Attribute mit doppelten Unterstrich __x durch den Python-Mechanismus name mangling in Attribute _A__x umbenannt.

Beipsiele: „Pythonic Way“

Hier wollen wir nun ein paar kleine Klassen anlegen um das richtige Muster kennen zu lernen.

class Person:
    def __init__(self, name):
        self._name = None # invernal variable, the setter is not called here.
        self.name = name # self.name class the @name.setter, variable initialization
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self,name):
        if not (isinstance(name,str)):
            raise ValueError("name must be a str")
        else:
            self._name = name # the internal _varible save the value
p1 = Person('Max')
print(p1.name)
p1.name = 'Jim'
print(p1.name)
Max
Jim
class Number:
    def __init__(self, value):
        self._value = None # invernal variable, the setter is not called here.
        self.value = value # self.value class the @name.setter, variable initialization
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self,value):
        if not (isinstance(value,int)):
            raise ValueError("value must be a int")
        else:
            self._value = value # the internal _varible save the value
n1 = Number(7)
print(n1.value)
n1.value = 7*7
print(n1.value)
7
49