Zum Inhalt springen
aviral gupta

// A4.5 · ca. 40 Min. · Vertiefung

Schwache Referenzen und ein zehnmal schnellerer Bericht

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

Ende des Moduls

Danach können Sie

  • Schwache Referenzen mit weakref.ref anlegen und wissen, was eine tote Referenz liefert und welche Objekte eine erlauben
  • Mit weakref.WeakValueDictionary einen Cache bauen, der genutzte Objekte teilt und ungenutzte freigibt
  • Einen Bericht Schritt für Schritt beschleunigen: messen, die heiße Stelle finden, beheben, das Ergebnis prüfen, ein Verhältnis vergleichen
  1. 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")))
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: Was gibt das aus?

    import gc
    import weakref
    
    
    class Report:
        pass
    
    
    r = Report()
    ref = weakref.ref(r)
    alive = ref() is r
    del r
    gc.collect()
    print(alive, ref())
  3. Üben · Aufgabe 3 von 7

    Setzen Sie die Klasse ein, damit der Cache einen Bericht verwirft, sobald nichts anderes ihn nutzt.

    cache = weakref.____()
    cache = weakref.()
  4. Üben · Aufgabe 4 von 7

    Auf welche Werte gibt es schwache Referenzen? Was gibt das aus?

    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))
  5. Üben · Aufgabe 5 von 7

    Bringen Sie die Schritte zum Beschleunigen eines langsamen Berichts in die richtige Reihenfolge.

    1. 1.Sie mit cProfile profilieren, sortiert nach kumulierter Zeit
    2. 2.Prüfen, dass die neue Version dasselbe Ergebnis liefert
    3. 3.Die heiße Stelle beheben, etwa eine Listensuche in einer Schleife
    4. 4.Die aktuelle Version mit timeit.repeat messen und das Minimum behalten
    5. 5.Erneut messen und das Verhältnis mit einer Schwelle vergleichen
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Was gibt das aus?

    import gc
    import weakref
    
    
    class Report:
        pass
    
    
    cache = weakref.WeakValueDictionary()
    cache["kept"] = kept = Report()
    cache["temp"] = Report()
    gc.collect()
    print(sorted(cache))
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Nehmen Sie eine Funktion, die in einer Schleife in einer Liste sucht, aus Ihrem eigenen Code oder wie slow() im Beispiel. Schreiben Sie eine schnelle Version mit einem einmal gebauten Dict. Geben Sie aus, ob beide dasselbe Ergebnis liefern, messen Sie dann beide mit timeit.repeat, behalten Sie jeweils das Minimum und geben Sie nur aus, ob das Verhältnis mindestens 10 ist. Nie rohe Sekunden ausgeben.

    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

Der langsame Bericht, schnell gemacht

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.py

Ausgabe

same customers: True
customer 7: 0.30000000000000004 vs 0.30
at least 10x faster: True
cached while in use: True
cached after del: False
  • Der langsame Bericht arbeitet sich durch etwa 13 Millionen Generatorschritte; der schnelle macht 3.000 Dict-Einträge und 9.000 Nachschläge.
  • Die float-Summe ist schon nach drei Bestellungen daneben; die Decimal-Summe ist exakt 0.30.
  • Das Verhältnis wird mit 10 verglichen, weit unter den gemessenen 30 bis 70, daher hält die Prüfung auch auf einem ausgelasteten Rechner.
  • Nach del und gc.collect() nutzt niemand den Bericht mehr, also hat der Cache ihn verworfen.
Ä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 3

Schritt 1: ein Durchlauf über die Kunden

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.

Hinweise
  1. Hinweis 1

    Die innere for-Schleife läuft einmal pro Bestellung: 500 Bestellungen bedeuten 500 Durchläufe über die Kunden.

  2. Hinweis 2

    Bauen Sie by_id = {customer.cid: customer for customer in customers} vor der Schleife über die Bestellungen.

  3. Hinweis 3

    Dann ist jeder Name by_id[cid].name, und eine List Comprehension über orders genügt.

Eine Lösung zeigen

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]
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

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.py

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

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 3

Schritt 2: exakte Summen

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.

Hinweise
  1. Hinweis 1

    Erst am Ende in Decimal umzuwandeln ist zu spät: Der float-Fehler steckt dann schon in der Summe.

  2. Hinweis 2

    Ändern Sie totals zu dict[int, Decimal] und addieren Sie Decimal(amount) zu totals.get(cid, Decimal("0")).

  3. Hinweis 3

    Ein Dict behält die Einfügereihenfolge, die Reihenfolge des ersten Auftretens gibt es also gratis; geben Sie totals direkt zurück.

Eine Lösung zeigen

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
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 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.py

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

python learnrun.py test
learnrun.py herunterladen

Übung 3 von 3

Schritt 3: ein Cache, der vergisst

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.

Hinweise
  1. Hinweis 1

    Nur die Zeile in __init__ muss sich ändern; get und __len__ funktionieren auf einem WeakValueDictionary genauso.

  2. Hinweis 2

    import weakref, dann self._reports: weakref.WeakValueDictionary[str, Report] = weakref.WeakValueDictionary().

  3. Hinweis 3

    get() auf einem WeakValueDictionary liefert None für einen verworfenen Eintrag, also baut der vorhandene Zweig if report is None ihn neu.

Eine Lösung zeigen

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)
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 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.py

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

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Eine schwache Referenz auf eine Liste

import weakref

rows = [1, 2, 3]
ref = weakref.ref(rows)

Was Python ausgibt

TypeError: cannot create weak reference to 'list' object

Warum, 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])).

Eine tote Referenz ohne Prüfung nutzen

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.

Einen verworfenen Cache-Eintrag mit [] lesen

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

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

Eine schwache Referenz hält nicht am Leben

ref = weakref.ref(obj) zeigt auf obj, ohne es zu besitzen. ref() liefert das Objekt, solange etwas anderes es noch hält, und None, sobald es weg ist. Prüfen Sie auf None, bevor Sie das Ergebnis nutzen. Instanzen eigener Klassen unterstützen schwache Referenzen; int, str, tuple, list und dict nicht, eine Unterklasse von list oder dict aber schon. In CPython verschwindet ein Objekt, sobald seine letzte starke Referenz weg ist; gc.collect() macht das in jeder Engine sicher.

Caches, die vergessen

Ein normaler Dict-Cache hält jeden Wert für immer am Leben. Ein weakref.WeakValueDictionary verwirft einen Eintrag, sobald nichts anderes seinen Wert nutzt: Ein großer Bericht wird geteilt, solange jemand damit arbeitet, und danach freigegeben. Lesen Sie Werte mit cache.get(key), das für einen verworfenen Eintrag None liefert, nicht mit cache[key], das KeyError auslöst. WeakKeyDictionary macht dasselbe für Schlüssel: Es hängt Daten an Objekte, ohne diese am Leben zu halten.

Schneller, gemessen

Beschleunigen Sie einen Bericht in dieser Reihenfolge. Messen Sie ihn mit timeit.repeat und behalten Sie das Minimum. Profilieren Sie ihn mit cProfile, um die heiße Stelle zu finden, typischerweise eine Listensuche in einer Schleife. Beheben Sie sie, etwa mit einem einmal gebauten Dict. Prüfen Sie, dass die neue Version dasselbe Ergebnis liefert, mit exakten Decimal-Beträgen, damit Rundung keinen Unterschied verdeckt. Dann messen Sie erneut und vergleichen das Verhältnis mit einer großzügigen Schwelle, nie rohe Sekunden, denn Zeiten schwanken bei jedem Lauf.

Quellen

Zuletzt geprüft am 29. September 2026