Python with-Anweisung und Kontextmanager
Lerne, wie die Python with-Anweisung und Kontextmanager funktionieren, wie du eigene mit __enter__/__exit__ schreibst und contextlib verwendest.
Die with-Anweisung stellt sicher, dass Ressourcen wie Dateien, Netzwerkverbindungen und Sperren korrekt eingerichtet und bereinigt werden — selbst wenn eine Ausnahme den Block unterbricht. Das Objekt, das dieses Einrichten und Aufräumen steuert, heißt Kontextmanager.
Dieses Kapitel erklärt, wie die with-Anweisung funktioniert, wann man sie einsetzt, wie man eigene Kontextmanager mit __enter__ und __exit__ schreibt und wie man mit contextlib.contextmanager einfache Varianten erstellt.
Warum with existiert
Vor der with-Anweisung erforderte die Ressourcenverwaltung das manuelle Schreiben von try/finally-Blöcken:
f = open("data.txt", "r", encoding="utf-8")
try:
content = f.read()
finally:
f.close() # must always close, even if read() raisesDas funktioniert, ist aber ausführlich, leicht zu vergessen und fügt um jede Ressource Boilerplate-Code hinzu. Die with-Anweisung fasst dies in einem einzigen, lesbaren Block zusammen und übernimmt die Bereinigung automatisch:
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# f is closed here, no matter what happened inside the blockDie as f-Klausel bindet den Wert des Kontextmanagers an den Namen f. Manche Kontextmanager liefern keinen nützlichen Wert, in diesem Fall kann as weggelassen werden:
with some_lock:
shared_data.append(item)Wie die with-Anweisung funktioniert
Wenn Python eine with-Anweisung ausführt, folgt es dieser Abfolge:
- Den Ausdruck nach
withauswerten — dies liefert das Kontextmanager-Objekt. - Die
__enter__()-Methode des Kontextmanagers aufrufen. Der Rückgabewert von__enter__()wird an dieas-Variable gebunden (sofern vorhanden). - Den Rumpf des
with-Blocks ausführen. - Die
__exit__(exc_type, exc_val, exc_tb)-Methode des Kontextmanagers aufrufen.- Wenn der Block normal abgeschlossen wurde, sind alle drei Argumente
None. - Wenn eine Ausnahme ausgelöst wurde, beschreiben die drei Argumente diese.
- Wenn
__exit__einen wahren Wert zurückgibt, wird die Ausnahme unterdrückt und die Ausführung wird nach demwith-Block fortgesetzt. Gibt es einen falschen Wert zurück (oderNone), wird die Ausnahme weitergegeben.
- Wenn der Block normal abgeschlossen wurde, sind alle drei Argumente
Dieses Protokoll wird als Kontextmanager-Protokoll bezeichnet.
Dateien mit with öffnen
Die häufigste Verwendung von with ist die Dateiverarbeitung. Pythons eingebaute Dateiobjekte implementieren das Kontextmanager-Protokoll, daher werden sie beim Beenden des Blocks automatisch geschlossen:
with open("report.txt", "w", encoding="utf-8") as f:
f.write("Sales: 1 000\n")
f.write("Returns: 23\n")
print(f.closed) # True — file was closed on exitWenn im Block eine Ausnahme auftritt, wird die Datei trotzdem geschlossen:
try:
with open("data.txt", "r", encoding="utf-8") as f:
raise RuntimeError("something went wrong")
except RuntimeError:
pass
print(f.closed) # True — closed despite the exceptionOhne with hinterlässt das Vergessen von f.close() nach einem Fehler den Dateideskriptor so lange geöffnet, bis der Garbage Collector läuft — oder bis der Prozess beendet wird — was in langlebigen Programmen zu Datenverlust oder Fehlermeldungen wie „too many open files" führen kann.
Mehrere Ressourcen gleichzeitig öffnen
Mehrere Ressourcen lassen sich in einer einzigen with-Anweisung öffnen, indem sie durch Kommas getrennt werden (Python 3.1+):
with open("input.txt", "r", encoding="utf-8") as src, \
open("output.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())Das ist exakt äquivalent zu zwei verschachtelten with-Anweisungen, hält die Einrückungstiefe aber flach.
Einen Kontextmanager mit __enter__ und __exit__ schreiben
Jede Klasse, die __enter__ und __exit__ definiert, kann mit der with-Anweisung verwendet werden. Hier ein minimales Beispiel — ein Timer, der misst, wie lange der with-Block läuft:
import time
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self._start
print(f"Elapsed: {elapsed:.4f}s")
return False # do not suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
# Elapsed: 0.0xxx s
print(total) # 499999500000Wichtige Punkte:
__enter__läuft vor dem Block. Es gibt den Wert zurück, der anas tgebunden wird. Die Rückgabe vonselfermöglicht dem Aufrufer bei Bedarf den Zugriff auft.elapsedund andere Attribute.__exit__läuft nach dem Block, auch bei einer Ausnahme. Die Rückgabe vonFalse(oderNone) lässt etwaige Ausnahmen normal weiterlaufen.
Ausnahmen in __exit__ unterdrücken
Wenn __exit__ True zurückgibt, wird die Ausnahme verschluckt und die Ausführung wird nach dem with-Block fortgesetzt. Dies ist in bestimmten Kontexten gewünscht — zum Beispiel bei einem Kontextmanager, der Fehler abfängt und protokolliert, ohne das Programm abstürzen zu lassen:
class Ignore:
"""Silently ignore any exception raised inside the with block."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"Suppressed: {exc_type.__name__}: {exc_val}")
return True # suppress the exception
with Ignore():
x = 1 / 0 # ZeroDivisionError is caught and ignored
print("execution continues here")
# Suppressed: ZeroDivisionError: division by zero
# execution continues hereVerwende die Ausnahmeunterdrückung mit Bedacht — das stille Verschlucken von Fehlern kann Bugs verbergen. contextlib.suppress aus der Standardbibliothek ist der idiomatische Weg dafür (siehe unten).
Ein Kontextmanager für Datenbankverbindungen
Ein realistischeres Beispiel — die Verwaltung einer datenbankähnlichen Verbindung, die bei Erfolg ein Commit und bei einem Fehler ein Rollback durchführt:
class ManagedTransaction:
def __init__(self, connection):
self.conn = connection
def __enter__(self):
self.conn.begin()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.commit()
else:
self.conn.rollback()
return False # always let exceptions propagateDas Muster — Commit bei Erfolg, Rollback bei Fehler — taucht in echten Datenbankbibliotheken überall auf (SQLite, SQLAlchemy und psycopg2 implementieren es alle).
contextlib.contextmanager: Generator-basierte Kontextmanager
Das Schreiben einer vollständigen Klasse mit __enter__ und __exit__ ist der richtige Ansatz für komplexe oder zustandsbehaftete Kontextmanager. Für einfachere Fälle ermöglicht der Dekorator contextlib.contextmanager dieselbe Logik als Generatorfunktion auszudrücken:
from contextlib import contextmanager
@contextmanager
def managed_open(path, mode="r", encoding="utf-8"):
print(f"Opening {path}")
f = open(path, mode, encoding=encoding)
try:
yield f # everything up to yield is __enter__
finally:
f.close() # everything after yield is __exit__
print(f"Closed {path}")
with managed_open("notes.txt", "w") as f:
f.write("hello\n")
# Opening notes.txt
# Closed notes.txtDas Generatorprotokoll wird direkt auf das Kontextmanager-Protokoll abgebildet:
- Code vor
yield→__enter__(Einrichtung). - Der
yield-Ausdruck → der Wert, der an dieas-Variable gebunden wird. - Code nach
yield(üblicherweise in einemfinally) →__exit__(Aufräumen).
Das try/finally um yield ist wichtig: ohne es würde eine Ausnahme im with-Block dazu führen, dass der Aufräum-Code nie ausgeführt wird.
contextmanager-Beispiel: Temporäres Arbeitsverzeichnis
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path):
original = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original)
with working_directory("/tmp"):
print(os.getcwd()) # /tmp (or system temp dir)
print(os.getcwd()) # restored to original directoryDieses Muster ist auch in der Standardbibliothek als tempfile.TemporaryDirectory verfügbar.
contextlib-Hilfsprogramme
Das contextlib-Modul enthält mehrere fertige Kontextmanager, die es wert sind, gekannt zu werden:
contextlib.suppress
Bestimmte Ausnahmen ohne Boilerplate unterdrücken:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error even if file does not existEntspricht einem try/except, das bei der abgefangenen Ausnahme nichts tut.
contextlib.nullcontext
Ein No-Op-Kontextmanager, der nützlich ist, wenn ein Kontextmanager optional sein soll:
from contextlib import nullcontext
def process(data, lock=None):
ctx = lock if lock is not None else nullcontext()
with ctx:
return sorted(data)Ohne nullcontext würde man jedes Mal eine if lock:-Verzweigung benötigen.
contextlib.ExitStack
ExitStack ermöglicht die Verwaltung einer dynamischen Anzahl von Kontextmanagern — nützlich, wenn die Anzahl der Ressourcen erst zur Laufzeit bekannt ist:
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [
stack.enter_context(open(f, "w", encoding="utf-8"))
for f in files
]
for i, fh in enumerate(handles):
fh.write(f"file {i}\n")
# All three files are closed hereExitStack ist auch das richtige Werkzeug, wenn ein Kontextmanager bedingt hinzugefügt werden soll oder wenn die Bereinigung auf einen späteren Zeitpunkt verschoben werden soll.
Wann with vs. Try/Finally verwenden
Verwende with immer dann, wenn:
- Eine Ressource nach der Verwendung freigegeben werden muss (Dateien, Sockets, Sperren, Datenbank-Cursor).
- die Bereinigung auch bei Ausnahmen garantiert sein soll.
- die Bereinigungslogik unabhängig von Erfolg oder Misserfolg immer gleich ist.
Verwende ein einfaches try/finally nur wenn:
- Verschiedene Bereinigungsaktionen je nach Ausnahmetyp benötigt werden — obwohl
__exit__das auch kann. - Python-2-kompatibler Code geschrieben wird (heute selten).
In der Praxis gilt: Wenn das Objekt das Kontextmanager-Protokoll unterstützt, ist with immer vorzuziehen.
Kurzreferenz
| Feature | Was es tut |
|---|---|
with expr as v: | Ruft expr.__enter__() auf, bindet das Ergebnis an v, ruft __exit__ beim Beenden auf |
| Mehrere Ressourcen | with A() as a, B() as b: — beide werden bereinigt, selbst wenn B() eine Ausnahme auslöst |
__enter__(self) | Einrichtung; der Rückgabewert wird an die as-Variable gebunden |
__exit__(self, exc_type, exc_val, exc_tb) | Aufräumen; True zurückgeben, um die Ausnahme zu unterdrücken |
@contextmanager | Wandelt eine Generatorfunktion in einen Kontextmanager um |
contextlib.suppress(E) | Unterdrückt den Ausnahmetyp E ohne try/except |
contextlib.nullcontext() | Platzhalter, wenn ein Kontextmanager optional ist |
contextlib.ExitStack | Verwaltet eine dynamische oder bedingte Menge von Kontextmanagern |
Verwandte Kapitel
- Python File Handling —
with open(...)für jede Dateioperation verwenden - Python Try Except — Ausnahmebehandlung und try/finally
- Python Decorators —
@contextmanagerverwendet dasselbe Dekoratormuster - Python Generators — wie das Generatorprotokoll
@contextmanagerantreibt - Python Classes and Objects —
__enter__- und__exit__-Methoden schreiben