W3docs

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() raises

Das 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 block

Die 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:

  1. Den Ausdruck nach with auswerten — dies liefert das Kontextmanager-Objekt.
  2. Die __enter__()-Methode des Kontextmanagers aufrufen. Der Rückgabewert von __enter__() wird an die as-Variable gebunden (sofern vorhanden).
  3. Den Rumpf des with-Blocks ausführen.
  4. 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 dem with-Block fortgesetzt. Gibt es einen falschen Wert zurück (oder None), wird die Ausnahme weitergegeben.

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 exit

Wenn 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 exception

Ohne 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)  # 499999500000

Wichtige Punkte:

  • __enter__ läuft vor dem Block. Es gibt den Wert zurück, der an as t gebunden wird. Die Rückgabe von self ermöglicht dem Aufrufer bei Bedarf den Zugriff auf t.elapsed und andere Attribute.
  • __exit__ läuft nach dem Block, auch bei einer Ausnahme. Die Rückgabe von False (oder None) 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 here

Verwende 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 propagate

Das 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.txt

Das Generatorprotokoll wird direkt auf das Kontextmanager-Protokoll abgebildet:

  • Code vor yield__enter__ (Einrichtung).
  • Der yield-Ausdruck → der Wert, der an die as-Variable gebunden wird.
  • Code nach yield (üblicherweise in einem finally) → __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 directory

Dieses 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 exist

Entspricht 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 here

ExitStack 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

FeatureWas es tut
with expr as v:Ruft expr.__enter__() auf, bindet das Ergebnis an v, ruft __exit__ beim Beenden auf
Mehrere Ressourcenwith 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
@contextmanagerWandelt 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.ExitStackVerwaltet eine dynamische oder bedingte Menge von Kontextmanagern

Verwandte Kapitel

Übungen

Übung
What method does a context manager call when the with block is entered?
What method does a context manager call when the with block is entered?
Übung
What happens when __exit__ returns True?
What happens when __exit__ returns True?
Übung
In a @contextmanager generator, code before the yield statement corresponds to which part of the context manager protocol?
In a @contextmanager generator, code before the yield statement corresponds to which part of the context manager protocol?
Übung
Which contextlib utility suppresses specific exceptions without a try/except block?
Which contextlib utility suppresses specific exceptions without a try/except block?
Was this page helpful?