2.24. Kontextmanager

Die with-Anweisung führt Einrichtungscode aus, dann einen Rumpf und dann Aufräumcode – mit der Garantie, dass das Aufräumen auch dann läuft, wenn der Rumpf mittendrin fehlschlägt. Das Paar von Methoden, das die Einrichtung und das Aufräumen bereitstellt, wird Kontextmanager genannt.

Die Form ist:

with <expression> as <name>:
    <body>

Der Ausdruck liefert einen Kontextmanager zurück. Python ruft dessen __enter__-Methode auf, bindet das Ergebnis optional an <name>, führt den Rumpf aus und ruft dann __exit__ auf – unabhängig davon, ob der Rumpf normal abgeschlossen wurde oder eine Ausnahme ausgelöst hat.

Ablaufdiagramm: __enter__ läuft zuerst, dann der Rumpf; der Rumpf wird entweder normal abgeschlossen oder löst eine Ausnahme aus; in beiden Fällen läuft am Ende __exit__.

__exit__ läuft am Ende des Blocks, was auch immer darin passiert.

2.24.1. Einen Kontextmanager verwenden

Das klassische Beispiel ist das Öffnen einer Datei:

with open("data.txt") as f:
    text = f.read()

# f is now closed, even if read() failed

open() gibt ein Dateiobjekt zurück, das selbst ein Kontextmanager ist; __enter__ gibt die Datei zurück, und __exit__ schließt sie. Der with-Block macht „die Datei nach Gebrauch immer schließen“ zum Standardverhalten, statt zu etwas, an das der Aufrufer denken muss.

2.24.1.1. Mehrere Kontextmanager

Eine einzelne with-Anweisung kann mehrere Kontextmanager auf einmal betreten, getrennt durch Kommas:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

Äquivalent zu zwei verschachtelten with-Blöcken, aber flacher. Die Manager werden von links nach rechts betreten und in umgekehrter Reihenfolge verlassen; löst __enter__ beim rechten Manager eine Ausnahme aus, läuft __exit__ des linken Managers dennoch.

2.24.2. Einen Kontextmanager schreiben

Jede Klasse mit __enter__- und __exit__-Methoden funktioniert als Kontextmanager:

class Section:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        print("---", self.label, "---")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("--- end", self.label, "---")
        return False

with Section("setup"):
    print("doing the work")

Ausgabe:

--- setup ---
doing the work
--- end setup ---

__exit__ erhält drei Argumente, die die Ausnahme beschreiben, die den Block beendet hat, oder drei None-Werte, falls der Block normal abgeschlossen wurde. Die Rückgabe von False (oder None) lässt jede Ausnahme nach dem Aufräumen weiterpropagieren; die Rückgabe von True würde sie verschlucken.

Verwenden Sie Kontextmanager für jede Ressource, die einen „Öffnen / Schließen“- oder „Anfordern / Freigeben“-Lebenszyklus hat – nicht nur für Dateien. Das Muster hält das Aufräumen mit der Einrichtung an der Stelle gepaart, an der beide eingeführt werden, sodass ein vergessenes Schließen mitten in einer langen Funktion die Ressource nicht lecken lassen kann.