Python @property: Getter und Setter
Python @property Decorator erklärt: Getter, Setter, Deleter und berechnete Attribute mit sauberer Syntax und vollständiger Validierung.
Der @property-Decorator ist Pythons eingebauter Mechanismus, um eine Methode in ein verwaltetes Attribut umzuwandeln. Anstatt get_x()- und set_x()-Methoden wie in anderen Sprachen zu schreiben, verwendet man einen ganz normalen Attributzugriff (obj.x) und behält dabei volle Kontrolle darüber, was beim Lesen, Schreiben oder Löschen des Attributs passiert.
Dieses Kapitel behandelt:
- Warum Properties existieren und wann man sie einsetzt
- Erstellen eines schreibgeschützten Property mit
@property - Hinzufügen eines Setters mit
@<name>.setter - Hinzufügen eines Deleters mit
@<name>.deleter - Berechnete (abgeleitete) Properties
- Umwandlung eines einfachen Attributs in ein Property ohne Änderung des aufrufenden Codes
- Die eingebaute Funktion
property()— der zugrundeliegende Mechanismus des Decorators - Wie Properties als Deskriptoren funktionieren (kurzer Blick unter die Haube)
- Häufige Fallstricke
Bevor Sie weiterlesen, sollten Sie mit Python-Klassen und -Objekten vertraut sein. Properties sind ein wichtiges Werkzeug für die Python-Kapselung. Für Methoden auf Klassenebene und statische Methoden, siehe @staticmethod und @classmethod.
Warum Properties existieren
Betrachten wir eine Klasse, die eine Temperatur in Celsius speichert. Eine naive Implementierung legt den internen Wert direkt offen:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
t = Temperature(25)
t.celsius = -5000 # nothing stops this — physically impossibleDas Problem: Nichts verhindert, dass Aufrufer eine Temperatur unterhalb des absoluten Nullpunkts (−273,15 °C) setzen. Man könnte eine set_celsius()-Methode mit Validierung hinzufügen, aber dann müssen Aufrufer ihren Code von t.celsius = 100 auf t.set_celsius(100) umschreiben — eine Änderung, die die API bricht.
@property löst dieses Problem elegant. Man behält die Syntax t.celsius = 100 bei und fügt im Hintergrund eine Kontrollschicht hinzu.
Einfacher Getter: Nur-Lese-Zugriff
Die einfachste Verwendung von @property ist ein schreibgeschütztes Attribut, das durch eine private Variable gesichert wird:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # store in a private attribute
@property
def celsius(self):
return self._celsiusDer @property-Decorator lässt celsius für den Aufrufer wie ein einfaches Attribut erscheinen:
t = Temperature(25)
print(t.celsius) # 25 — no parentheses; Python calls the getter automaticallyDa kein Setter vorhanden ist, löst der Versuch einer Zuweisung einen Fehler aus:
t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setterDies ist die korrekte Art, einen Wert zu modellieren, der nur zur Konstruktionszeit oder über spezifische Methoden gesetzt werden soll.
Setter mit Validierung hinzufügen
Dekorieren Sie eine zweite Methode mit @<property_name>.setter, um Schreibzugriffe zu behandeln:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = valueJetzt funktionieren sowohl Lese- als auch Schreibzugriffe mit einfacher Attributsyntax:
t = Temperature(25)
print(t.celsius) # 25
t.celsius = 100
print(t.celsius) # 100
t.celsius = -300 # ValueError: Temperature below absolute zeroWichtige Regel: Setter und Getter müssen denselben Namen tragen (in beiden Fällen celsius). Der Decorator @celsius.setter verknüpft die neue Methode mit dem bestehenden celsius-Property-Objekt.
Berechnete Properties
Ein Property muss überhaupt nicht einem gespeicherten Attribut entsprechen. Es kann einen Wert zur Laufzeit aus anderen Daten berechnen:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32fahrenheit hat keine eigene Variable — es leitet seinen Wert bei jedem Lesen aus _celsius ab:
t = Temperature(0)
print(t.fahrenheit) # 32.0
t.celsius = 100
print(t.fahrenheit) # 212.0Da kein @fahrenheit.setter vorhanden ist, führt der Versuch t.fahrenheit = 100 zu einem AttributeError. Berechnete Properties sind von Natur aus schreibgeschützt, sofern kein Setter explizit hinzugefügt wird.
Ein reales Beispiel für ein berechnetes Property
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError('Width must be positive')
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError('Height must be positive')
self._height = value
@property
def area(self):
return self._width * self._height # computed; no setter
@property
def perimeter(self):
return 2 * (self._width + self._height) # computed; no setter
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
r.width = 10
print(r.area) # 50
r.width = -1 # ValueError: Width must be positiveDeleter hinzufügen
Der Decorator @<property_name>.deleter ermöglicht es, Code auszuführen, wenn der Aufrufer del obj.attr verwendet:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero')
self._celsius = value
@celsius.deleter
def celsius(self):
print('Deleting celsius')
del self._celsius
t = Temperature(25)
del t.celsius # Deleting celsius
print(t.celsius) # AttributeError: 'Temperature' object has no attribute '_celsius'Deleter werden seltener eingesetzt als Getter und Setter. Sie sind nützlich, wenn:
- Ein zwischengespeicherter Wert entfernt werden soll, um beim nächsten Zugriff eine Neuberechnung zu erzwingen.
- Ressourcen, die an ein Attribut gebunden sind, explizit freigegeben werden sollen.
- Sichergestellt werden soll, dass ein einmal gelöschter Wert nicht ohne erneute Zuweisung wieder gelesen werden kann.
Ein einfaches Attribut in ein Property umwandeln
Einer der größten praktischen Vorteile von @property besteht darin, dass man mit einem einfachen öffentlichen Attribut beginnen und später Validierung hinzufügen kann, ohne bestehenden Aufrufcode zu ändern. Dies wird manchmal als das Prinzip des einheitlichen Zugriffs bezeichnet.
# Version 1 — plain attribute, no validation
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
print(c.radius) # 5
c.radius = 10 # works, but nothing stops c.radius = -1Später benötigt man Validierung. Mit @property lässt sich diese hinzufügen, ohne die Aufrufer anzupassen:
# Version 2 — property with validation; public interface unchanged
import math
class Circle:
def __init__(self, radius):
self.radius = radius # this now calls the setter
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
@property
def area(self):
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5
print(f'{c.area:.4f}') # 78.5398
c.radius = 10
print(c.radius) # 10
c.radius = -1 # ValueError: Radius cannot be negativeJeder bestehende Code, der c.radius liest oder schreibt, funktioniert weiterhin ohne Änderung.
Die eingebaute Funktion property()
@property ist syntaktischer Zucker für die eingebaute Funktion property(). Diese beiden Definitionen sind gleichwertig:
# --- decorator style (recommended) ---
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value# --- property() style (explicit) ---
class Person:
def __init__(self, age):
self._age = age
def _get_age(self):
return self._age
def _set_age(self, value):
if not isinstance(value, int) or value < 0:
raise ValueError('Age must be a non-negative integer')
self._age = value
def _del_age(self):
del self._age
age = property(_get_age, _set_age, _del_age, 'The person\'s age in years')property(fget, fset, fdel, doc) akzeptiert bis zu vier Argumente: eine Getter-Funktion, eine Setter-Funktion, eine Deleter-Funktion und einen Docstring. Jedes davon kann None sein.
p = Person(30)
print(p.age) # 30
p.age = 31
print(p.age) # 31
print(Person.age.__doc__) # The person's age in yearsDie Decorator-Form ist übersichtlicher und die Standardempfehlung. Der explizite property()-Aufruf ist nützlich, wenn man den Docstring ohne einen mehrzeiligen Decorator-Block übergeben möchte oder wenn die Accessor-Funktionen bereits unter einem anderen Namen vorhanden sind.
Wie Properties funktionieren: Ein kurzer Blick auf Deskriptoren
Intern ist property ein Deskriptor — ein Objekt, das __get__, __set__ und __delete__ auf der Klasse definiert. Wenn Python obj.attr nachschlägt, prüft es, ob das Attribut auf der Klasse ein Deskriptor ist, und ruft in diesem Fall dessen __get__ auf, anstatt den Wert direkt zurückzugeben.
Man kann dies sehen, indem man das Property-Objekt auf der Klasse inspiziert:
class Square:
def __init__(self, side):
self._side = side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
if value < 0:
raise ValueError('Side must be non-negative')
self._side = value
print(type(Square.side)) # <class 'property'>
print(Square.side.fget) # <function Square.side at 0x...>
print(Square.side.fset) # <function Square.side at 0x...>
print(Square.side.fdel) # NoneDeshalb gibt das Lesen von Square.side das Property-Objekt selbst zurück (Deskriptor auf der Klasse aufgerufen), während das Lesen von s.side auf einer Instanz __get__ auslöst und den Integer-Wert zurückgibt. Das Deskriptor-Protokoll ist derselbe Mechanismus, den classmethod, staticmethod und Funktionen selbst verwenden. Für einen tieferen Einblick, siehe Python Magic Methods.
Häufige Fallstricke
Endlose Rekursion: Den Unterstrich vergessen
Ein sehr häufiger Fehler ist die Verwendung desselben Namens für das Property und das dahinterliegende Attribut:
class Bad:
@property
def value(self):
return self.value # RecursionError! This calls the getter again
@value.setter
def value(self, v):
self.value = v # RecursionError! This calls the setter againDen Sicherungswert immer unter einem anderen Namen speichern, gemäß Konvention mit einem Unterstrich als Präfix:
class Good:
@property
def value(self):
return self._value # reads the private attribute
@value.setter
def value(self, v):
self._value = v # writes the private attributeSetter vor Getter definiert
Der Setter-Decorator @celsius.setter referenziert das celsius-Property-Objekt, das zuerst existieren muss. Den Getter (@property) immer vor Setter und Deleter im Klassenrumpf definieren.
__init__ ruft den Setter automatisch auf
Wenn man self.radius = radius innerhalb von __init__ schreibt, ruft Python den Setter auf (sofern einer vorhanden ist). Das ist normalerweise gewünscht — die Validierung wird auch zur Konstruktionszeit ausgeführt. Aber das bedeutet, dass der Setter die anfängliche Zuweisung korrekt verarbeiten muss:
class Circle:
def __init__(self, radius):
self.radius = radius # triggers the setter — validation applies here too
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError('Radius cannot be negative')
self._radius = value
Circle(-1) # ValueError: Radius cannot be negativeProperties sind auf Klassenebene, nicht auf Instanzebene
Man kann ein Property nicht einer einzelnen Instanz hinzufügen, wie es bei regulären Attributen möglich ist. Properties werden auf der Klasse definiert und gelten für alle Instanzen. Wenn man eine attributbezogene Anpassung pro Instanz benötigt, siehe Python Dataclasses oder verwenden Sie einen __slots__-basierten Ansatz.
Kurzreferenz
| Syntax | Was es bewirkt |
|---|---|
@property | Definiert den Getter; das Attribut wird schreibgeschützt, bis ein Setter hinzugefügt wird |
@<name>.setter | Definiert den Setter; das Attribut wird les- und schreibbar |
@<name>.deleter | Definiert den Deleter; del obj.attr löst diese Methode aus |
property(fget, fset, fdel, doc) | Gleichwertiges eingebautes Konstrukt ohne Decorator-Syntax |
ClassName.prop.fget | Die zugrundeliegende Getter-Funktion |
ClassName.prop.fset | Die zugrundeliegende Setter-Funktion (None wenn kein Setter) |
ClassName.prop.fdel | Die zugrundeliegende Deleter-Funktion (None wenn kein Deleter) |