Zum Inhalt springen
aviral gupta

// A1.4 · ca. 30 Min. · Vertiefung

Kontextmanager schreiben

Nach dieser Lektion schreiben Sie Klassen, die in einer with-Anweisung funktionieren, auf jedem Ausgangsweg aufräumen und genau die Ausnahmen unterdrücken, die Sie wählen.

Lektion 4 von 6 in A1 Das Datenmodell

Danach können Sie

  • __enter__ und __exit__ schreiben und den Rückgabewert von __enter__ mit as nutzen
  • Vorhersagen, wann __exit__ läuft und welche drei Argumente es erhält
  • Ausnahmen gezielt durch return True unterdrücken, und nur für die richtigen Typen
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul I1: In with open("data.txt") as f: löst eine Zeile ValueError aus. Was passiert mit der Datei?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: Was gibt das aus?

    class Tag:
        def __init__(self, name):
            self.name = name
    
        def __enter__(self):
            print(f"<{self.name}>", end=" ")
            return self
    
        def __exit__(self, exc_type, exc, tb):
            print(f"</{self.name}>")
    
    
    with Tag("p") as t:
        print("hello from", t.name, end=" ")
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den Methodennamen, sodass with Session() as s: den Namen s an "cursor-1" bindet.

    class Session:
        def ____(self):
            return "cursor-1"
    
        def __exit__(self, exc_type, exc, tb):
            pass
    def (self):
  4. Üben · Aufgabe 4 von 7

    Der Block endet normal. Was gibt __exit__ aus?

    class Probe:
        def __enter__(self):
            return self
    
        def __exit__(self, exc_type, exc, tb):
            print(exc_type, exc, tb)
    
    
    with Probe():
        pass
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jeder Situation zu, was passiert.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Quiet unterdrückt jede Ausnahme. Was gibt load() zurück?

    class Quiet:
        def __enter__(self):
            return self
    
        def __exit__(self, exc_type, exc, tb):
            return True
    
    
    def load():
        with Quiet():
            value = int("x")
            return value
        return -1
    
    
    print(load())
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie eine Klasse Indenter, deren Objekt sich in verschachtelten with-Anweisungen nutzen lässt. Jedes with erhöht beim Eintritt die Ebene um eins und senkt sie beim Austritt, und eine Methode print(text) gibt den Text nach so vielen Bindestrichen aus, wie die aktuelle Ebene angibt.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Eine Transaktion auf einem Dictionary

Transaction gibt dem Block über as eine Entwurfskopie. Endet der Block, kopiert __exit__ den Entwurf zurück: committed. Löst der Block eine Ausnahme aus, verwirft __exit__ den Entwurf und gibt None zurück; der Aufrufer sieht den Fehler, der Bestand bleibt unverändert. Lösen Sie im ersten Block eine Ausnahme aus, oder lassen Sie __exit__ True zurückgeben.

main.py

from types import TracebackType


class Transaction:
    """Changes a copy of the data; commits it only if the block succeeds."""

    def __init__(self, data: dict[str, int]) -> None:
        self.data = data

    def __enter__(self) -> dict[str, int]:
        self.draft = dict(self.data)
        return self.draft

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        if exc_type is None:
            self.data.clear()
            self.data.update(self.draft)
            print("committed")
        else:
            # Returning None (false) lets the exception continue.
            print(f"rolled back after {exc_type.__name__}: {exc}")


stock = {"apples": 5, "pears": 2}

with Transaction(stock) as draft:
    draft["apples"] -= 3
print(stock)

try:
    with Transaction(stock) as draft:
        draft["apples"] -= 1
        draft["pears"] -= 5
        if draft["pears"] < 0:
            raise ValueError("not enough pears")
except ValueError:
    print("the caller still sees the ValueError")
print(stock)

Ausführen mit

python main.py

Ausgabe

committed
{'apples': 2, 'pears': 2}
rolled back after ValueError: not enough pears
the caller still sees the ValueError
{'apples': 2, 'pears': 2}
  • as draft bindet die Kopie, die __enter__ zurückgibt, nicht die Transaction.
  • Der zweite Block hat auch apples im Entwurf geändert, und diese Änderung wurde verworfen.
  • __exit__ liest exc_type.__name__ und exc, um zu melden, was schiefging.
  • Weil __exit__ None zurückgibt, erreicht der ValueError das try um das with.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Tags, die sich immer schließen

Tag(name, out) schreibt Zeilen in die Liste out. Ergänzen Sie __enter__, das "<name>" anhängt und self zurückgibt, und __exit__, das "</name>" anhängt. Die schließende Zeile muss auch dann geschrieben werden, wenn der Block eine Ausnahme auslöst, und die Ausnahme muss den Aufrufer trotzdem erreichen.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    __exit__ hat vier Parameter: self, exc_type, exc und tb.

  2. Hinweis 2

    Python ruft __exit__ auch nach einer Ausnahme auf; das Anhängen dort genügt, ein try ist nicht nötig.

  3. Hinweis 3

    Lassen Sie __exit__ nichts zurückgeben: None ist falsch, also wird die Ausnahme nicht unterdrückt.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from types import TracebackType


class Tag:
    """Writes an opening line on entry and the closing line on exit."""

    def __init__(self, name: str, out: list[str]) -> None:
        self.name = name
        self.out = out

    def __enter__(self) -> "Tag":
        self.out.append(f"<{self.name}>")
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> None:
        self.out.append(f"</{self.name}>")


if __name__ == "__main__":
    lines: list[str] = []
    with Tag("ul", lines):
        with Tag("li", lines) as item:
            item.out.append("tea")
    print(lines)
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from types import TracebackType


class Tag:
    """Writes an opening line on entry and the closing line on exit."""

    def __init__(self, name: str, out: list[str]) -> None:
        self.name = name
        self.out = out

    # Add __enter__: append "<name>" to self.out and return self.
    # Add __exit__: append "</name>", even when the block raised,
    # and do not suppress the exception.


if __name__ == "__main__":
    lines: list[str] = []
    with Tag("ul", lines):
        with Tag("li", lines) as item:
            item.out.append("tea")
    print(lines)

test_main.py

from main import Tag


def test_as_gives_the_tag():
    """with Tag(...) as t bindet den Tag selbst"""
    out = []
    tag = Tag("p", out)
    with tag as t:
        assert t is tag, f"das as-Ziel ist {t!r}, erwartet war der Tag: geben Sie self aus __enter__ zurück"


def test_nested_lines():
    """Verschachtelte Tags öffnen und schließen in der richtigen Reihenfolge"""
    out = []
    with Tag("ul", out):
        with Tag("li", out):
            out.append("tea")
    assert out == ["<ul>", "<li>", "tea", "</li>", "</ul>"], f"die Zeilen sind {out!r}"


def test_closes_on_error():
    """Die schließende Zeile wird auch bei einer Ausnahme im Block geschrieben"""
    out = []
    try:
        with Tag("p", out):
            raise ValueError("boom")
    except ValueError:
        pass
    assert out == ["<p>", "</p>"], f"nach einem Fehler sind die Zeilen {out!r}, erwartet war ['<p>', '</p>']"


def test_does_not_suppress():
    """Eine Ausnahme im Block erreicht weiterhin den Aufrufer"""
    try:
        with Tag("p", []):
            raise ValueError("boom")
    except ValueError:
        return
    raise AssertionError("der ValueError wurde verschluckt: __exit__ darf nicht True zurückgeben")

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Ein wählerisches Ignore

Vervollständigen Sie __exit__ in Ignore. Es unterdrückt eine Ausnahme nur, wenn sie eine Instanz eines der Typen in self.types ist (Unterklassen zählen mit), und speichert das Ausnahmeobjekt dann in self.caught. Jede andere Ausnahme muss den Aufrufer erreichen, und nach einem sauberen Block bleibt caught None.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Bei sauberem Ende ist exc_type None, und issubclass(None, ...) würde scheitern; prüfen Sie das zuerst.

  2. Hinweis 2

    issubclass akzeptiert ein Tupel von Klassen: issubclass(exc_type, self.types).

  3. Hinweis 3

    if exc_type is not None and issubclass(exc_type, self.types): self.caught = exc; return True. Sonst return False.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from types import TracebackType


class Ignore:
    """Suppresses the given exception types and remembers what it caught."""

    def __init__(self, *types: type[BaseException]) -> None:
        self.types = types
        self.caught: BaseException | None = None

    def __enter__(self) -> "Ignore":
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> bool:
        if exc_type is not None and issubclass(exc_type, self.types):
            self.caught = exc
            return True
        return False


if __name__ == "__main__":
    prices: dict[str, int] = {}
    with Ignore(KeyError) as guard:
        prices["tea"]
    print("caught:", repr(guard.caught))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from types import TracebackType


class Ignore:
    """Suppresses the given exception types and remembers what it caught."""

    def __init__(self, *types: type[BaseException]) -> None:
        self.types = types
        self.caught: BaseException | None = None

    def __enter__(self) -> "Ignore":
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> bool:
        # Suppress (return True) only exceptions of self.types or their
        # subclasses, and store the exception in self.caught.
        return False


if __name__ == "__main__":
    prices: dict[str, int] = {}
    with Ignore(KeyError) as guard:
        prices["tea"]
    print("caught:", repr(guard.caught))

test_main.py

from main import Ignore


def test_suppresses_listed_type():
    """Eine angegebene Ausnahme wird unterdrückt und gespeichert"""
    with Ignore(ValueError) as guard:
        raise ValueError("bad number")
    assert isinstance(guard.caught, ValueError), f"guard.caught ist {guard.caught!r}, erwartet war der ValueError"


def test_subclasses_count():
    """Ignore(LookupError) unterdrückt auch KeyError"""
    with Ignore(LookupError) as guard:
        {}["missing"]
    assert isinstance(guard.caught, KeyError), f"guard.caught ist {guard.caught!r}, erwartet war ein KeyError"


def test_other_types_propagate():
    """Eine nicht angegebene Ausnahme erreicht den Aufrufer"""
    try:
        with Ignore(ValueError):
            raise KeyError("id")
    except KeyError:
        return
    raise AssertionError("Ignore(ValueError) hat einen KeyError verschluckt")


def test_clean_block():
    """Ohne Ausnahme bleibt caught None"""
    with Ignore(ValueError) as guard:
        pass
    assert guard.caught is None, f"guard.caught ist {guard.caught!r} nach einem sauberen Block"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

__enter__ ohne return

class LogFile:
    def __init__(self):
        self.lines = []

    def __enter__(self):
        self.lines.append("start")

    def __exit__(self, exc_type, exc, tb):
        self.lines.append("end")


with LogFile() as log:
    log.lines.append("working")

Was Python ausgibt

AttributeError: 'NoneType' object has no attribute 'lines'

Warum, und die Lösung

Der Name nach as erhält, was __enter__ zurückgibt, und eine Funktion ohne return gibt None zurück. Beenden Sie __enter__ mit return self, oder geben Sie die Ressource zurück, die der Block nutzen soll.

__exit__ mit falschen Parametern

class Tag:
    def __enter__(self):
        return self

    def __exit__(self):
        print("closing")


with Tag():
    print("inside")

Was Python ausgibt

TypeError: Tag.__exit__() takes 1 positional argument but 4 were given

Warum, und die Lösung

Python übergibt __exit__ immer drei Argumente: exc_type, exc und tb, nach einem sauberen Block jeweils None. Schreiben Sie def __exit__(self, exc_type, exc, tb):. Der Fehler kommt erst, nachdem der Block schon gelaufen ist.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Die zwei Methoden

with manager as target: ruft manager.__enter__() auf und bindet den Rückgabewert an target. Oft ist das self, es kann aber alles sein: open() liefert die Datei, eine Transaktion vielleicht eine Entwurfskopie. Fehlt das return, ist target None. Am Ende des Blocks ruft Python __exit__(exc_type, exc, tb) auf. Die Klasse object stellt keine der beiden Methoden bereit; eine Klasse ohne __exit__ wird mit TypeError abgewiesen.

__exit__ läuft immer

Sobald __enter__ ohne Fehler zurückgekehrt ist, läuft __exit__ garantiert: nach der letzten Zeile, nach return, break oder continue und nach einer Ausnahme. Bei sauberem Ende sind alle drei Argumente None; nach einer Ausnahme sind es ihr Typ, das Ausnahmeobjekt und der Traceback. Prüfen Sie also exc_type is None. Löst __enter__ selbst eine Ausnahme aus, startet der Block nie und __exit__ wird nicht aufgerufen.

Gezielt unterdrücken

Der Rückgabewert von __exit__ zählt nur nach einer Ausnahme. Ein wahrer Wert unterdrückt sie: Die Ausführung geht nach der with-Anweisung weiter, der Rest des Blocks entfällt. None oder False lässt die Ausnahme weiterlaufen; lösen Sie sie nicht selbst erneut aus. Unterdrücken Sie eng, mit issubclass(exc_type, allowed), und geben Sie nicht versehentlich einen wahren Wert zurück, etwa eine nicht leere Liste.

Quellen

Zuletzt geprüft am 29. September 2026