Python-Kurs / Decorators und Kontextmanager mit klaren Rückgaben einsetzen

Ein Decorator umschließt das Aufrufverhalten einer Funktion; ein Kontextmanager steuert Vorbereitung und Aufräumen rund um einen Block. Beide helfen, gemeinsame Abläufe auszulagern, doch Rückgabewerte und Ausnahmen der ursprünglichen Arbeit müssen dabei bewusst erhalten oder verändert werden.

Funktionshülle und Lebensdauer unterscheiden

@announce ist keine bloße Kennzeichnung. Nach der Definition wird die Funktion an announce übergeben und ihr Name an dessen Ergebnis gebunden. Vereinfacht entspricht dies einer anschließenden Zuweisung total = announce(total). Im Beispiel liefert der Decorator eine neue Funktion zurück, die vor dem eigentlichen Aufruf eine Meldung ausgibt.

Ein with-Block hat eine andere Aufgabe: Er ruft den Eintritts- und Austrittsablauf eines Kontextmanagers auf. Im Beispiel erhält der Block eine vorübergehende Liste. Beim Verlassen wird dieselbe Liste geleert, auch wenn im Block eine Ausnahme entstanden ist. Das ist eine Regel für diese Liste, keine automatische Rücknahme aller Aktionen des Blocks.

functools.wraps hilft, Metadaten wie Namen und Dokumentation der ursprünglichen Funktion zu bewahren. Es prüft nicht automatisch deren Typvertrag oder Sicherheit. Die Referenz zu wraps beschreibt die übernommenen Metadaten. Die Referenz zu contextmanager erläutert die Blockverwaltung mit einem Generator.

Eine vollständige Hülle mit temporärer Liste ausführen

Speichern Sie das vollständige Programm als UTF-8-Datei wrappers.py in einem eigenen Übungsordner. Die Standardbibliothek genügt. Starten Sie in der Projektumgebung python wrappers.py; außerhalb einer virtuellen Umgebung kann unter macOS oder Ubuntu der Interpreterbefehl python3 heißen.

from functools import wraps
from contextlib import contextmanager

def announce(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("start")
        return func(*args, **kwargs)
    return wrapper

@contextmanager
def temporary_items():
    items = []
    try:
        yield items
    finally:
        items.clear()
        print("cleared")

@announce
def total(amounts):
    return sum(amounts)

with temporary_items() as items:
    items.extend([3500, 8000])
    print(total(items))
print(total.__name__)
print(items)

Erwartete Ausgabe:

start
11500
cleared
total
[]

Beim Eintritt wird die neue Liste über yield items an den Block gegeben. Der Block fügt 3500 und 8000 hinzu und ruft die dekorierte Funktion auf. Deren Hülle gibt zuerst start aus, leitet die Argumente weiter und liefert die Summe 11.500 zurück. Beim Austritt wird die Liste geleert und cleared ausgegeben.

Durch wraps lautet der Funktionsname weiterhin total, statt den Namen der Hülle zu zeigen. Die Variable items ist nach dem Block im umgebenden Bereich weiterhin gebunden; sie zeigt auf dieselbe, inzwischen leere Liste. Ein Kontextmanager entfernt nicht automatisch die Variable aus dem Namensraum.

Rückgaben und Ausnahmen durch beide Ebenen verfolgen

Die innere Funktion nimmt *args und **kwargs entgegen und reicht sie unverändert an func weiter. Entscheidend ist außerdem return func(...): Ohne dieses return würde die Hülle das ursprüngliche Ergebnis verwerfen und selbst None zurückgeben. Eine sichtbare Startmeldung beweist daher noch keinen korrekt erhaltenen Aufrufvertrag.

Der mit contextmanager dekorierte Generator muss genau einmal einen Wert liefern. Vor diesem yield steht die Vorbereitung, währenddessen läuft der Block, danach folgt dessen Abschluss. Eine unbehandelte Ausnahme des Blocks wird an der yield-Stelle in den Generator zurückgegeben. Hier führt der finally-Block die Listenbereinigung aus, ohne die ursprüngliche Ausnahme zu unterdrücken.

Wenn Sie um yield eine Ausnahme abfangen und nicht erneut auslösen, kann der Kontextmanager sie als behandelt melden. Das kann eine absichtliche Wiederherstellung sein, aber auch einen Fehler verbergen. Die Übung zur Ausnahmebehandlung erläutert den Unterschied zwischen Aufräumen und dem Verschlucken eines Fehlers.

Auch die Bereinigung selbst kann scheitern. Sie ist kein Schutz vor jedem möglichen Prozessabbruch und keine Transaktion über externe Nebenwirkungen. Das Leeren der Liste macht beispielsweise eine schon gesendete Anfrage oder gespeicherte Datei nicht rückgängig. Wählen Sie daher einen eng beschriebenen Verantwortungsbereich für jeden Kontextmanager.

Bereinigung und passende Abstraktion prüfen

  1. Erstellen Sie eine separate Kopie und lösen Sie im with-Block nach dem Füllen der Liste absichtlich ValueError aus. Es muss weiterhin cleared erscheinen. Die Ausnahme soll den Block anschließend verlassen.
  2. Wenn Sie die Ausnahme außerhalb des Blocks abfangen, prüfen Sie danach die Liste. Sie muss leer sein; die Bereinigung und die Fehlerweitergabe sind zwei getrennte Beobachtungen.
  3. Entfernen Sie vorübergehend nur das return vor func(*args, **kwargs). Der Aufruf druckt weiterhin start, sein Ergebnis wird jedoch None. Stellen Sie den ursprünglichen Code danach wieder her.
  4. Prüfen Sie einen Aufruf mit benanntem Argument sowie eine Ausnahme aus der umschlossenen Funktion. Beides muss so weitergereicht werden, wie der Vertrag der Hülle es vorsieht.

Ein Retry-Decorator verdient eine eigene Entscheidung: Eine wiederholte Schreibanfrage kann doppelte Vorgänge erzeugen. Vermischen Sie eine Wiederholungsregel deshalb nicht unbemerkt mit einer Hülle, die angeblich nur protokolliert oder aufräumt. Ein kleiner einmaliger Ablauf kann ohne zusätzliche Abstraktion verständlicher bleiben.

Verwenden Sie den Kontextmanager mit einem jeweils neuen Aufruf wie temporary_items(). Das so erzeugte einzelne Generator-Kontextobjekt ist für einen Durchlauf vorgesehen. Wenn mehrere unabhängige Durchläufe benötigt werden, erzeugen Sie jeweils ein neues Objekt und prüfen Sie, dass deren temporäre Listen nicht geteilt werden.