W3docs

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 impossible

Das 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._celsius

Der @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 automatically

Da kein Setter vorhanden ist, löst der Versuch einer Zuweisung einen Fehler aus:

t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setter

Dies 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 = value

Jetzt 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 zero

Wichtige 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 + 32

fahrenheit 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.0

Da 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 positive

Deleter 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 = -1

Spä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 negative

Jeder 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 years

Die 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)     # None

Deshalb 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 again

Den 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 attribute

Setter 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 negative

Properties 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

SyntaxWas es bewirkt
@propertyDefiniert den Getter; das Attribut wird schreibgeschützt, bis ein Setter hinzugefügt wird
@<name>.setterDefiniert den Setter; das Attribut wird les- und schreibbar
@<name>.deleterDefiniert den Deleter; del obj.attr löst diese Methode aus
property(fget, fset, fdel, doc)Gleichwertiges eingebautes Konstrukt ohne Decorator-Syntax
ClassName.prop.fgetDie zugrundeliegende Getter-Funktion
ClassName.prop.fsetDie zugrundeliegende Setter-Funktion (None wenn kein Setter)
ClassName.prop.fdelDie zugrundeliegende Deleter-Funktion (None wenn kein Deleter)

Übungen

Übung
Which decorator do you use to define a setter for a property named `age`?
Which decorator do you use to define a setter for a property named `age`?
Übung
What happens when you assign to a property that has only a getter defined?
What happens when you assign to a property that has only a getter defined?
Übung
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
Was this page helpful?