Kontext-Manager ermöglichen es Python, sicherzustellen, dass eine Ressource bereinigt wird, egal ob der Codeblock erfolgreich ist oder fehlschlägt. Ein technischer Artikel erläutert ihre Funktionsweise mit der Anweisung with sowie verschiedene Möglichkeiten, sie zu erstellen und zu nutzen, weit über die Dateiverwaltung hinaus.
Viele Entwickler lernen diesen Mechanismus mit with open(path) kennen, wodurch die Datei automatisch geschlossen wird. Laut Autor sind Dateien jedoch der am wenigsten interessante Anwendungsfall: Die with-Anweisung grenzt hauptsächlich einen Block ab, dessen Verlassen garantiert ist. Das Protokoll basiert auf zwei Methoden, __enter__(), die beim Eintritt in den Block aufgerufen wird, und __exit__(), die beim Verlassen aufgerufen wird, sei es normal, durch ein return oder aufgrund einer Exception.
Der von __exit__() zurückgegebene Wert spielt eine entscheidende Rolle. Wenn er wahr ist, wird die Exception unterdrückt, was unbemerkt bleiben kann: Das Programm läuft weiter und der Fehler verschwindet spurlos. Wenn er False ist oder fehlt, steigt die Exception nach der Bereinigung wieder auf. Deshalb geben die im Artikel vorgestellten Kontext-Manager niemals einen wahren Wert zurück.
Eigene Kontext-Manager erstellen
Eine Klasse eignet sich, wenn der Kontext-Manager einen Zustand speichern muss, den der Aufrufer später auslesen kann. Der Artikel gibt das Beispiel eines Timers, der die Dauer eines Blocks mit time.perf_counter() misst und das Ergebnis anzeigt, selbst wenn der Block fehlschlägt. Eine unterbrochene Feature-Extraktion zeigt so « feature extraction: 12.418s (failed) » an, bevor der Fehler wieder aufsteigt.
Für eine lineare Aufgabe, also eine Vorbereitung, einen Block und dann eine Wiederherstellung, erlaubt der Dekorator @contextmanager das Schreiben einer einfachen Generatorfunktion. Alles vor dem yield wird beim Eintritt ausgeführt, alles danach beim Verlassen. Der Artikel empfiehlt, die Wiederherstellung in einem finally-Block zu platzieren und eine Exception niemals in einem try/except zu verschlucken, ohne sie erneut auszulösen. Der yield darf nur einen einzigen Wert produzieren, andernfalls wird ein RuntimeError ausgelöst.
Zustand wiederherstellen und mehrere Ressourcen verwalten
Die Standardbibliothek bietet bereits mehrere Kontext-Manager. TemporaryDirectory() erstellt ein Verzeichnis mit eindeutigem Namen und löscht es am Ende des Blocks, wodurch Restdateien von vorherigen Läufen vermieden werden. Seit Python 3.12 erlaubt der Parameter delete=False, die Dateien für eine spätere Analyse zu behalten. Unter Windows kann eine noch geöffnete Datei das Löschen scheitern lassen; der seit Python 3.10 verfügbare Parameter ignore_cleanup_errors=True mildert dieses Problem, löst es aber nicht.
Für Änderungen an Dictionaries stellt patch.dict() aus dem Modul unittest.mock den Originalzustand nach dem Block wieder her: Ein vorhandener Schlüssel erhält seinen alten Wert zurück, ein fehlender Schlüssel wird gelöscht. Der Artikel weist jedoch darauf hin, dass os.environ prozessglobal ist. Alle Tasks und Threads sehen dann die Änderung, was sie bei nebenläufigem Code riskant macht.
Wenn die Anzahl der Ressourcen erst zur Laufzeit bekannt ist, beispielsweise die Anzahl der zu mergenden Dateien, gruppiert ExitStack die Kontext-Manager. Seine Methode enter_context() registriert einen Kontext-Manager und callback() eine einfache Funktion. Beim Verlassen werden die Elemente in umgekehrter Reihenfolge vom Stapel entfernt. Der Artikel präzisiert, dass dieses Entfernen am Ende des Blocks oder bei einem Aufruf von close() erfolgt, nicht wenn das Objekt vom Garbage Collector gelöscht wird. Eine dedizierte Klasse kann vorzuziehen sein, wenn der Stapel zu komplex wird.
