Aufwärmen · Aufgabe 1 von 7
Aufwärmen aus der letzten Lektion: Was gibt das aus?
from decimal import Decimal
print(repr(Decimal("0.10") + Decimal("0.20")))// A4.5 · ca. 40 Min. · Vertiefung
Nach dieser Lektion können Sie mit weakref.ref und WeakValueDictionary Objekte teilen, ohne sie am Leben zu halten, und einen langsamen Bericht zehnmal schneller machen, geprüft und gemessen.
Lektion 5 von 5 in A4 Leistung und Zahlen
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
from decimal import Decimal
print(repr(Decimal("0.10") + Decimal("0.20")))Vorhersagen · Aufgabe 2 von 7
import gc
import weakref
class Report:
pass
r = Report()
ref = weakref.ref(r)
alive = ref() is r
del r
gc.collect()
print(alive, ref())Üben · Aufgabe 3 von 7
cache = weakref.____()Üben · Aufgabe 4 von 7
import weakref
class Rows(list):
pass
ok = []
for value in (Rows([1, 2]), [1, 2], 42, "text"):
try:
weakref.ref(value)
ok.append(type(value).__name__)
except TypeError:
pass
print(" ".join(ok))Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
import gc
import weakref
class Report:
pass
cache = weakref.WeakValueDictionary()
cache["kept"] = kept = Report()
cache["temp"] = Report()
gc.collect()
print(sorted(cache))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
Der langsame Bericht durchsucht für jede Bestellung die Kundenliste und addiert float-Beträge. Der schnelle baut einmal ein Dict nach id und addiert exakte Decimals. Das Programm prüft zuerst, dass beide dieselben Kunden finden, und zeigt die Summen; dann misst es beide und gibt nur aus, ob das Verhältnis 10 übertrifft; tatsächlich liegt es zwischen 30 und 70. Zum Schluss teilt ein WeakValueDictionary den fertigen Bericht, ohne ihn am Leben zu halten.
main.py
import gc
import timeit
import weakref
from decimal import Decimal
class Customer:
def __init__(self, cid: int, name: str) -> None:
self.cid = cid
self.name = name
class Report:
def __init__(self, totals: dict[str, Decimal]) -> None:
self.totals = totals
customers = [Customer(i, f"customer {i}") for i in range(3000)]
orders = [(i % 3000, "0.10") for i in range(9000)]
def slow_totals() -> dict[str, float]:
totals: dict[str, float] = {}
for cid, amount in orders:
customer = next(c for c in customers if c.cid == cid) # scans the list
totals[customer.name] = totals.get(customer.name, 0.0) + float(amount)
return totals
def fast_totals() -> dict[str, Decimal]:
by_id = {c.cid: c for c in customers} # one pass, then O(1) lookups
totals: dict[str, Decimal] = {}
for cid, amount in orders:
name = by_id[cid].name
totals[name] = totals.get(name, Decimal("0")) + Decimal(amount)
return totals
# 1. Same report? Compare before timing anything.
slow, fast = slow_totals(), fast_totals()
print("same customers:", slow.keys() == fast.keys())
print("customer 7:", slow["customer 7"], "vs", fast["customer 7"])
# 2. Measured: best of 3 runs each, reported only as a ratio.
t_slow = min(timeit.repeat(slow_totals, number=1, repeat=3))
t_fast = min(timeit.repeat(fast_totals, number=1, repeat=3))
print("at least 10x faster:", t_slow / t_fast >= 10)
# 3. A weak cache: reports are shared while in use, never kept alive by it.
cache: weakref.WeakValueDictionary[str, Report] = weakref.WeakValueDictionary()
report = Report(fast)
cache["2026-09"] = report
print("cached while in use:", cache.get("2026-09") is report)
del report
gc.collect()
print("cached after del:", "2026-09" in cache)
Ausführen mit
python main.pyAusgabe
same customers: True
customer 7: 0.30000000000000004 vs 0.30
at least 10x faster: True
cached while in use: True
cached after del: FalseTab 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 3
customer_names(orders, customers) liefert für jede Bestellung (Kunden-id, Betrag) den Kundennamen. Der Starter durchsucht für jede Bestellung die Kundenliste, und genau das zeigt das Profil eines großen Berichts als heiße Stelle. Bauen Sie einmal ein Dict von id auf Kunde und schlagen Sie jede Bestellung darin nach. Ein Test zählt, wie oft über die Kundenliste iteriert wird: höchstens einmal.
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.
Die innere for-Schleife läuft einmal pro Bestellung: 500 Bestellungen bedeuten 500 Durchläufe über die Kunden.
Bauen Sie by_id = {customer.cid: customer for customer in customers} vor der Schleife über die Bestellungen.
Dann ist jeder Name by_id[cid].name, und eine List Comprehension über orders genügt.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
class Customer:
def __init__(self, cid: int, name: str) -> None:
self.cid = cid
self.name = name
def customer_names(orders: list[tuple[int, str]], customers: list[Customer]) -> list[str]:
"""The customer's name for every (customer id, amount) order, in order."""
by_id = {customer.cid: customer for customer in customers}
return [by_id[cid].name for cid, _amount in orders]
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
class Customer:
def __init__(self, cid: int, name: str) -> None:
self.cid = cid
self.name = name
def customer_names(orders: list[tuple[int, str]], customers: list[Customer]) -> list[str]:
"""The customer's name for every (customer id, amount) order, in order."""
names = []
for cid, _amount in orders:
for customer in customers:
if customer.cid == cid:
names.append(customer.name)
break
return names
test_main.py
from main import Customer, customer_names
class CountingList(list):
"""Eine Liste, die zählt, wie oft über sie iteriert wird."""
loops = 0
def __iter__(self):
CountingList.loops += 1
return super().__iter__()
def test_names():
"""Jede Bestellung bekommt den Namen ihres Kunden, in Reihenfolge"""
customers = [Customer(1, "Ada"), Customer(2, "Bo")]
got = customer_names([(2, "5.00"), (1, "3.50"), (2, "1.00")], customers)
assert got == ["Bo", "Ada", "Bo"], f"Ergebnis {got!r}, erwartet ['Bo', 'Ada', 'Bo']"
def test_one_pass_over_customers():
"""500 Bestellungen durchlaufen die Kundenliste einmal, nicht einmal pro Bestellung"""
customers = CountingList(Customer(i, f"c{i}") for i in range(100))
orders = [(i % 100, "1.00") for i in range(500)]
CountingList.loops = 0
got = customer_names(orders, customers)
assert got[:2] == ["c0", "c1"] and len(got) == 500, "die Namen stimmen nicht"
assert CountingList.loops <= 1, f"über die Kundenliste wurde {CountingList.loops}-mal iteriert; bauen Sie einmal ein Dict nach id"
def test_no_orders():
"""Keine Bestellungen: eine leere Liste"""
got = customer_names([], [Customer(1, "Ada")])
assert got == [], f"Ergebnis {got!r}, erwartet []"
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 3
revenue(orders) addiert die Beträge pro Kunden-id. Der Starter addiert floats und wandelt erst am Ende um, so werden drei Bestellungen zu 0.10 zu 0.3000000000000000444089209850062616169452667236328125. Addieren Sie stattdessen Decimals aus den Betrags-Strings, jede Summe beginnend bei Decimal("0"), und behalten Sie die Kunden in der Reihenfolge ihres ersten Auftretens.
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.
Erst am Ende in Decimal umzuwandeln ist zu spät: Der float-Fehler steckt dann schon in der Summe.
Ändern Sie totals zu dict[int, Decimal] und addieren Sie Decimal(amount) zu totals.get(cid, Decimal("0")).
Ein Dict behält die Einfügereihenfolge, die Reihenfolge des ersten Auftretens gibt es also gratis; geben Sie totals direkt zurück.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
from decimal import Decimal
def revenue(orders: list[tuple[int, str]]) -> dict[int, Decimal]:
"""Exact revenue per customer id from (customer id, amount) orders."""
totals: dict[int, Decimal] = {}
for cid, amount in orders:
totals[cid] = totals.get(cid, Decimal("0")) + Decimal(amount)
return totals
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 decimal import Decimal
def revenue(orders: list[tuple[int, str]]) -> dict[int, Decimal]:
"""Exact revenue per customer id from (customer id, amount) orders."""
totals: dict[int, float] = {}
for cid, amount in orders:
totals[cid] = totals.get(cid, 0.0) + float(amount)
return {cid: Decimal(total) for cid, total in totals.items()}
test_main.py
from decimal import Decimal
from main import revenue
def test_exact_cents():
"""Drei Bestellungen zu 0.10 ergeben genau 0.30"""
got = revenue([(7, "0.10"), (7, "0.10"), (7, "0.10")])
assert got == {7: Decimal("0.30")}, f"Ergebnis {got!r}, erwartet {{7: Decimal('0.30')}}"
assert str(got[7]) == "0.30", f"Ergebnis {str(got[7])!r}, erwartet '0.30' mit beiden Stellen"
def test_per_customer():
"""Jede Kunden-id bekommt ihre eigene Summe, in Reihenfolge des ersten Auftretens"""
got = revenue([(2, "19.99"), (1, "5.00"), (2, "0.01")])
assert list(got) == [2, 1], f"Schlüssel {list(got)!r}, erwartet [2, 1]"
assert got[2] == Decimal("20.00") and got[1] == Decimal("5.00"), f"Ergebnis {got!r}"
def test_no_orders():
"""Keine Bestellungen: ein leeres Dict"""
assert revenue([]) == {}, "keine Bestellungen sollten {} ergeben"
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 3 von 3
ReportCache.get(key, build) liefert den gecachten Bericht für key oder ruft build() auf und speichert das Ergebnis. Der Starter speichert Berichte in einem Dict, also wird nie einer freigegeben. Speichern Sie sie in einem weakref.WeakValueDictionary: Ein genutzter Bericht wird geteilt und nur einmal gebaut, und sobald ihn niemand mehr nutzt, verschwindet der Eintrag und das nächste get baut ihn neu.
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.
Nur die Zeile in __init__ muss sich ändern; get und __len__ funktionieren auf einem WeakValueDictionary genauso.
import weakref, dann self._reports: weakref.WeakValueDictionary[str, Report] = weakref.WeakValueDictionary().
get() auf einem WeakValueDictionary liefert None für einen verworfenen Eintrag, also baut der vorhandene Zweig if report is None ihn neu.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import weakref
from collections.abc import Callable
from decimal import Decimal
class Report:
def __init__(self, totals: dict[int, Decimal]) -> None:
self.totals = totals
class ReportCache:
"""Shares reports that are still in use, without keeping any of them alive."""
def __init__(self) -> None:
self._reports: weakref.WeakValueDictionary[str, Report] = weakref.WeakValueDictionary()
def get(self, key: str, build: Callable[[], Report]) -> Report:
report = self._reports.get(key)
if report is None:
report = build()
self._reports[key] = report
return report
def __len__(self) -> int:
return len(self._reports)
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 decimal import Decimal
class Report:
def __init__(self, totals: dict[int, Decimal]) -> None:
self.totals = totals
class ReportCache:
"""Shares reports that are still in use, without keeping any of them alive."""
def __init__(self) -> None:
self._reports: dict[str, Report] = {}
def get(self, key: str, build: Callable[[], Report]) -> Report:
report = self._reports.get(key)
if report is None:
report = build()
self._reports[key] = report
return report
def __len__(self) -> int:
return len(self._reports)
test_main.py
import gc
from decimal import Decimal
from main import Report, ReportCache
class Builder:
"""Baut einen Bericht und zählt, wie oft das nötig war."""
def __init__(self):
self.calls = 0
def __call__(self):
self.calls += 1
return Report({1: Decimal("0.30")})
def test_shares_live_report():
"""Solange ein Bericht genutzt wird, liefert get dasselbe Objekt und baut einmal"""
cache, build = ReportCache(), Builder()
first = cache.get("2026-09", build)
second = cache.get("2026-09", build)
assert second is first, "get hat einen zweiten Bericht gebaut, obwohl der erste noch genutzt wird"
assert build.calls == 1, f"build wurde {build.calls}-mal aufgerufen, erwartet 1"
def test_does_not_keep_alive():
"""Sobald niemand den Bericht nutzt, gibt der Cache ihn frei"""
cache = ReportCache()
report = cache.get("2026-09", Builder())
del report
gc.collect()
assert len(cache) == 0, f"der Cache hält noch {len(cache)} Bericht(e); speichern Sie sie in einem weakref.WeakValueDictionary"
def test_rebuilds_after_drop():
"""Ein verworfener Bericht wird beim nächsten get neu gebaut"""
cache, build = ReportCache(), Builder()
cache.get("2026-09", build)
gc.collect()
report = cache.get("2026-09", build)
assert build.calls == 2, f"build wurde {build.calls}-mal aufgerufen, erwartet 2"
assert report.totals == {1: Decimal("0.30")}, "der neu gebaute Bericht hat falsche Summen"
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 herunterladenimport weakref
rows = [1, 2, 3]
ref = weakref.ref(rows)
Was Python ausgibt
TypeError: cannot create weak reference to 'list' objectWarum, und die Lösung
list, dict, int, str und tuple unterstützen keine schwachen Referenzen. Verpacken Sie die Daten in eine Instanz einer eigenen Klasse oder bilden Sie eine Unterklasse von list: class Rows(list): pass, dann klappt weakref.ref(Rows([1, 2, 3])).
import gc
import weakref
class Report:
def __init__(self) -> None:
self.name = "September"
ref = weakref.ref(Report())
gc.collect()
print(ref().name)
Was Python ausgibt
AttributeError: 'NoneType' object has no attribute 'name'Warum, und die Lösung
Der Bericht hatte keine starke Referenz, also ist er weg und ref() liefert None. Rufen Sie die Referenz einmal auf, halten Sie das Ergebnis in einer Variablen und prüfen Sie es: report = ref(), dann if report is not None: report.name nutzen.
import gc
import weakref
class Report:
pass
cache = weakref.WeakValueDictionary()
cache["temp"] = Report()
gc.collect()
print(cache["temp"])
Was Python ausgibt
KeyError: 'temp'Warum, und die Lösung
Einträge eines WeakValueDictionary verschwinden, wenn ihr Wert stirbt, sogar zwischen zwei Zeilen Ihres Codes. Nutzen Sie cache.get("temp"), das None liefert, und bauen Sie den Wert dann neu, wie ReportCache.get es tut.
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.