Zum Inhalt springen
aviral gupta

// A1.5 · ca. 30 Min. · Vertiefung

Deskriptoren und Properties

Nach dieser Lektion schreiben Sie wiederverwendbare verwaltete Attribute als Deskriptoren, sagen voraus, ob Deskriptor oder Instanz-Dictionary eine Suche gewinnt, und erklären, was property wirklich ist.

Lektion 5 von 6 in A1 Das Datenmodell

Danach können Sie

  • Einen Deskriptor mit __get__, __set__ und __set_name__ schreiben, der Werte pro Instanz speichert
  • Daten- von Nicht-Daten-Deskriptoren unterscheiden und vorhersagen, welche das Instanz-Dictionary überdeckt
  • property als Daten-Deskriptor erklären und Getter und Setter richtig verwenden
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul I2: Eine Instanz weist ein Attribut zu, das auch die Klasse hat. Was gibt das aus?

    class Box:
        size = 1
    
    
    b = Box()
    b.size = 2
    print(b.size, Box.size)
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: Das Klassenattribut x ist ein Objekt mit einer Methode __get__. Was gibt das aus?

    class Ten:
        def __get__(self, instance, owner):
            return 10
    
    
    class A:
        x = Ten()
    
    
    print(A().x, A.x)
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Hook, den Python beim Erzeugen der Klasse P aufruft, damit das Field erfährt, dass es an age zugewiesen wurde.

    class Field:
        def ____(self, owner, name):
            self.private = "_" + name
    def (self, owner, name):
  4. Üben · Aufgabe 4 von 7

    Both definiert __get__ und __set__. Was gibt das aus?

    class Both:
        def __get__(self, instance, owner):
            return "descriptor"
    
        def __set__(self, instance, value):
            pass  # ignores the value
    
    
    class A:
        x = Both()
    
    
    a = A()
    a.x = "instance"
    a.__dict__["x"] = "dict"
    print(a.x, a.__dict__["x"])
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Begriff seine Bedeutung zu.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das Instanz-Dictionary erhält einen Eintrag mit demselben Namen wie eine schreibgeschützte Property. Was liefert a.x?

    class A:
        @property
        def x(self):
            return "property"
    
    
    a = A()
    a.__dict__["x"] = "dict"
    print(a.x)
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie einen Deskriptor Typed(kind), der nur Werte des angegebenen Typs annimmt und sonst TypeError mit der Meldung "<name> must be <Typname>" auslöst. Verwenden Sie ihn für zwei Attribute einer Klasse Order: qty = Typed(int) und note = Typed(str).

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Ein prüfender Deskriptor neben einer Property

Positive prüft jede Zuweisung an price und qty, für jede Klasse, die ihn nutzt. __set_name__ gibt jeder Instanz ihren eigenen Speichernamen, daher zeigt vars(item) _price und _qty. total ist eine schreibgeschützte Property, berechnet aus beiden. Setzen Sie probeweise item.price = -1, oder lesen Sie LineItem.price.

main.py

from typing import Any


class Positive:
    """A data descriptor that only accepts numbers greater than zero."""

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name
        self.private = "_" + name

    def __get__(self, instance: object, owner: type | None = None) -> Any:
        if instance is None:
            return self  # accessed on the class: LineItem.price
        return getattr(instance, self.private)

    def __set__(self, instance: object, value: float) -> None:
        if value <= 0:
            raise ValueError(f"{self.name} must be > 0, got {value}")
        setattr(instance, self.private, value)


class LineItem:
    price = Positive()
    qty = Positive()

    def __init__(self, name: str, price: float, qty: int) -> None:
        self.name = name
        self.price = price  # calls Positive.__set__
        self.qty = qty

    @property
    def total(self) -> float:
        return self.price * self.qty


item = LineItem("tea", 4.5, 2)
print(item.total)
print(vars(item))
try:
    item.qty = 0
except ValueError as err:
    print(err)
print(type(LineItem.__dict__["price"]).__name__, type(LineItem.__dict__["total"]).__name__)

Ausführen mit

python main.py

Ausgabe

9.0
{'name': 'tea', '_price': 4.5, '_qty': 2}
qty must be > 0, got 0
Positive property
  • self.price = price in __init__ läuft schon über Positive.__set__, also wird sogar der Konstruktor geprüft.
  • Die beiden Positive-Objekte teilen sich alle LineItems; nur die Werte im Instanz-Dictionary unterscheiden sich.
  • Das Klassen-Dictionary enthält ein Positive-Objekt und ein property-Objekt: beides Deskriptoren.
  • Der Typhinweis Any an __get__ erspart dem einfachen mypy die Frage, ob der Deskriptor oder ein Wert zurückkommt. mypy --strict aus Modul A2 meldet trotzdem total, das dieses Any als float zurückgibt: [no-any-return].
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Ein Attribut mit Bereichsprüfung

Vervollständigen Sie den Deskriptor Bounded(low, high). __set_name__ speichert Werte unter "_" + name, __get__ gibt beim Zugriff über die Klasse den Deskriptor selbst zurück, sonst den gespeicherten Wert, und __set__ löst für Werte außerhalb von low..high (einschließlich) ValueError aus. Thermostat nutzt ihn zweimal mit verschiedenen Bereichen.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Speichern Sie in __set_name__ den Namen (für Meldungen) und den privaten Namen "_" + name.

  2. Hinweis 2

    Prüfen Sie in __get__ zuerst instance is None; sonst getattr(instance, self.private).

  3. Hinweis 3

    Testen Sie in __set__ self.low <= value <= self.high, bevor Sie setattr(instance, self.private, value) aufrufen.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from typing import Any


class Bounded:
    """A number that must stay between low and high (inclusive)."""

    def __init__(self, low: float, high: float) -> None:
        self.low = low
        self.high = high

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name
        self.private = "_" + name

    def __get__(self, instance: object, owner: type | None = None) -> Any:
        if instance is None:
            return self
        return getattr(instance, self.private)

    def __set__(self, instance: object, value: float) -> None:
        if not self.low <= value <= self.high:
            raise ValueError(f"{self.name} must be between {self.low} and {self.high}")
        setattr(instance, self.private, value)


class Thermostat:
    target = Bounded(5, 30)
    eco = Bounded(5, 20)

    def __init__(self, target: float, eco: float) -> None:
        self.target = target
        self.eco = eco


if __name__ == "__main__":
    t = Thermostat(21, 17)
    print(t.target, t.eco)
    try:
        t.target = 40
    except ValueError as err:
        print(err)
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from typing import Any


class Bounded:
    """A number that must stay between low and high (inclusive)."""

    def __init__(self, low: float, high: float) -> None:
        self.low = low
        self.high = high

    # Add __set_name__: remember the name and store values under "_" + name.
    # Add __get__: return the descriptor itself on class access, else the value.
    # Add __set__: raise ValueError outside low..high, else store the value.


class Thermostat:
    target = Bounded(5, 30)
    eco = Bounded(5, 20)

    def __init__(self, target: float, eco: float) -> None:
        self.target = target
        self.eco = eco


if __name__ == "__main__":
    t = Thermostat(21, 17)
    print(t.target, t.eco)
    try:
        t.target = 40
    except ValueError as err:
        print(err)

test_main.py

from main import Bounded, Thermostat


def test_get_and_set():
    """Werte lassen sich innerhalb des Bereichs lesen und ändern"""
    t = Thermostat(21, 17)
    t.target = 25
    assert (t.target, t.eco) == (25, 17), f"erhalten {(t.target, t.eco)!r}, erwartet (25, 17)"


def test_rejects_out_of_range():
    """Ein Wert außerhalb von low..high löst ValueError aus"""
    t = Thermostat(21, 17)
    try:
        t.eco = 25
    except ValueError:
        assert t.eco == 17, f"nach der gescheiterten Zuweisung ist eco {t.eco!r}, erwartet 17"
        return
    raise AssertionError("t.eco = 25 wurde angenommen, aber eco erlaubt nur 5 bis 20")


def test_stored_under_private_names():
    """Jeder Wert liegt im Instanz-Dictionary unter _ plus seinem Namen"""
    t = Thermostat(21, 17)
    assert vars(t) == {"_target": 21, "_eco": 17}, f"vars(t) ist {vars(t)!r}"


def test_instances_are_independent():
    """Zwei Thermostate behalten ihre eigenen Werte"""
    a = Thermostat(21, 17)
    b = Thermostat(10, 8)
    assert (a.target, b.target) == (21, 10), f"erhalten {(a.target, b.target)!r}: werden die Werte auf dem Deskriptor gespeichert?"


def test_class_access_returns_descriptor():
    """Thermostat.target liefert das Bounded-Objekt selbst"""
    assert isinstance(Thermostat.target, Bounded), f"Thermostat.target ist {Thermostat.target!r}"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Ein faules Attribut

lazy ist ein Nicht-Daten-Deskriptor, der als Dekorator dient. Er berechnet den Wert bisher bei jedem Zugriff. Sorgen Sie dafür, dass er nur einmal rechnet: Speichern Sie das Ergebnis im Instanz-Dictionary unter dem Attributnamen. Weil lazy kein __set__ hat, findet die nächste Suche diesen Eintrag zuerst und ruft __get__ nie wieder auf.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    __set_name__ speichert den Attributnamen bereits in self.name.

  2. Hinweis 2

    instance.__dict__[self.name] = value schreibt direkt ins Instanz-Dictionary, ohne dass ein Deskriptor beteiligt ist.

  3. Hinweis 3

    Fügen Sie kein __set__ hinzu: Dann wäre lazy ein Daten-Deskriptor, und der Eintrag im Instanz-Dictionary würde ignoriert.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from collections.abc import Callable
from typing import Any


class lazy:
    """Computes the value on first access and caches it on the instance."""

    def __init__(self, func: Callable[[Any], Any]) -> None:
        self.func = func

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name

    def __get__(self, instance: object, owner: type | None = None) -> Any:
        if instance is None:
            return self
        value = self.func(instance)
        instance.__dict__[self.name] = value
        return value


class Report:
    def __init__(self, rows: list[int]) -> None:
        self.rows = rows
        self.runs = 0

    @lazy
    def total(self) -> int:
        self.runs += 1
        return sum(self.rows)


if __name__ == "__main__":
    report = Report([3, 4, 5])
    print(report.total, report.total, report.runs)
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from collections.abc import Callable
from typing import Any


class lazy:
    """Computes the value on first access and caches it on the instance."""

    def __init__(self, func: Callable[[Any], Any]) -> None:
        self.func = func

    def __set_name__(self, owner: type, name: str) -> None:
        self.name = name

    def __get__(self, instance: object, owner: type | None = None) -> Any:
        if instance is None:
            return self
        # Store the value so that later lookups never reach __get__ again.
        return self.func(instance)


class Report:
    def __init__(self, rows: list[int]) -> None:
        self.rows = rows
        self.runs = 0

    @lazy
    def total(self) -> int:
        self.runs += 1
        return sum(self.rows)


if __name__ == "__main__":
    report = Report([3, 4, 5])
    print(report.total, report.total, report.runs)

test_main.py

from main import Report, lazy


def test_value():
    """total ist die Summe der Zeilen"""
    assert Report([1, 2, 3]).total == 6, f"total ist {Report([1, 2, 3]).total!r}, erwartet 6"


def test_computed_once():
    """Die Funktion läuft nur beim ersten Zugriff"""
    report = Report([1, 2])
    for _ in range(3):
        report.total
    assert report.runs == 1, f"die Funktion lief {report.runs}-mal, erwartet einmal"


def test_cached_in_instance_dict():
    """Nach dem ersten Zugriff liegt der Wert im Instanz-Dictionary"""
    report = Report([4])
    report.total
    assert vars(report).get("total") == 4, f"vars(report) ist {vars(report)!r}"


def test_uses_the_attribute_name():
    """Der Cache-Schlüssel ist der Name, dem der Deskriptor zugewiesen wurde"""
    class Box:
        @lazy
        def size(self):
            return 7

    box = Box()
    box.size
    assert vars(box) == {"size": 7}, f"vars(box) ist {vars(box)!r}, erwartet {{'size': 7}}"


def test_class_access_returns_descriptor():
    """Report.total liefert das lazy-Objekt selbst"""
    assert isinstance(Report.total, lazy), f"Report.total ist {Report.total!r}"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Eine Property, die sich selbst liest

class Product:
    @property
    def price(self):
        return self.price


print(Product().price)

Was Python ausgibt

RecursionError: maximum recursion depth exceeded

Warum, und die Lösung

Im Getter ist self.price wieder die Property, also ruft sie sich selbst auf, bis der Stack erschöpft ist. Legen Sie den Wert unter einem anderen Namen ab, etwa self._price, und lesen Sie diesen im Getter.

Ein Setter mit anderem Namen

class Product:
    def __init__(self, price):
        self.price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def set_price(self, value):
        self._price = value


Product(5)

Was Python ausgibt

AttributeError: property 'price' of 'Product' object has no setter

Warum, und die Lösung

@price.setter liefert eine neue Property mit Setter, und das def bindet sie an den Namen set_price. Unter price steht weiterhin die alte, schreibgeschützte Property. Geben Sie dem Setter denselben Namen wie dem Getter: def price(self, value).

__get__ ohne den Fall des Klassenzugriffs

class Field:
    def __set_name__(self, owner, name):
        self.private = "_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self.private)

    def __set__(self, instance, value):
        setattr(instance, self.private, value)


class P:
    age = Field()


print(P.age)

Was Python ausgibt

AttributeError: 'NoneType' object has no attribute '_age'

Warum, und die Lösung

Beim Zugriff über die Klasse ruft Python __get__ mit instance gleich None auf. Beginnen Sie __get__ mit if instance is None: return self, damit P.age den Deskriptor liefert, wie help() und andere Werkzeuge es erwarten.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Objekt in der Klasse, das für ein Attribut antwortet

Ein Deskriptor ist ein Objekt, das als Klassenattribut gespeichert ist und dessen Klasse __get__, __set__ oder __delete__ definiert. Das Lesen von obj.x ruft __get__(obj, type(obj)) auf, das Lesen von Cls.x ruft __get__(None, Cls) auf; geben Sie in diesem Fall self zurück. __set_name__(owner, name) läuft einmal beim Erzeugen der Besitzerklasse und nennt dem Deskriptor seinen Attributnamen. Alle Instanzen teilen sich den Deskriptor, also speichern Sie jeden Wert auf der Instanz, etwa unter "_" + name.

Daten- und Nicht-Daten-Deskriptoren

Ein Deskriptor mit __set__ oder __delete__ ist ein Daten-Deskriptor, einer mit nur __get__ ein Nicht-Daten-Deskriptor. Für obj.x prüft Python zuerst Daten-Deskriptoren, dann das Instanz-Dictionary, dann Nicht-Daten-Deskriptoren und gewöhnliche Klassenvariablen. Ein Daten-Deskriptor lässt sich also nicht durch obj.__dict__ überdecken, ein Nicht-Daten-Deskriptor schon. Funktionen sind Nicht-Daten-Deskriptoren: Ihr __get__ liefert eine gebundene Methode, daher kommt self.

property ist ein Daten-Deskriptor

property(fget, fset, fdel) baut einen Daten-Deskriptor, der Ihre Funktionen aufruft. @property erzeugt den Getter; @name.setter liefert eine Kopie der Property mit Setter, also muss der Setter denselben Namen tragen. Ohne Setter löst eine Zuweisung AttributeError aus. Nehmen Sie property für ein Attribut einer Klasse; schreiben Sie eine Deskriptor-Klasse, wenn dieselbe Regel für viele Attribute gilt.

Quellen

Zuletzt geprüft am 29. September 2026