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)// A1.5 · ca. 30 Min. · Vertiefung
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.
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
class Box:
size = 1
b = Box()
b.size = 2
print(b.size, Box.size)Vorhersagen · Aufgabe 2 von 7
class Ten:
def __get__(self, instance, owner):
return 10
class A:
x = Ten()
print(A().x, A.x)Üben · Aufgabe 3 von 7
class Field:
def ____(self, owner, name):
self.private = "_" + nameÜben · Aufgabe 4 von 7
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"])Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
class A:
@property
def x(self):
return "property"
a = A()
a.__dict__["x"] = "dict"
print(a.x)Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
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
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.pyAusgabe
9.0
{'name': 'tea', '_price': 4.5, '_qty': 2}
qty must be > 0, got 0
Positive propertyTab 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.
Übung 1 von 2
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.
Speichern Sie in __set_name__ den Namen (für Meldungen) und den privaten Namen "_" + name.
Prüfen Sie in __get__ zuerst instance is None; sonst getattr(instance, self.private).
Testen Sie in __set__ self.low <= value <= self.high, bevor Sie setattr(instance, self.private, value) aufrufen.
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)
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
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.
__set_name__ speichert den Attributnamen bereits in self.name.
instance.__dict__[self.name] = value schreibt direkt ins Instanz-Dictionary, ohne dass ein Deskriptor beteiligt ist.
Fügen Sie kein __set__ hinzu: Dann wäre lazy ein Daten-Deskriptor, und der Eintrag im Instanz-Dictionary würde ignoriert.
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)
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenclass Product:
@property
def price(self):
return self.price
print(Product().price)
Was Python ausgibt
RecursionError: maximum recursion depth exceededWarum, 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.
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 setterWarum, 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).
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
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.