Zum Inhalt springen
aviral gupta

// I1.5 · ca. 40 Min. · Aufbau

Build: ein Dateileser mit Wiederholung und contextlib

In diesem Build schreiben Sie eigene Context Manager mit @contextmanager, nutzen suppress und closing und verbinden das Modul zu einem Dateileser, der es erneut versucht, jeden Fehlversuch meldet und seine Sperrdatei immer entfernt.

Lektion 5 von 5 in I1 Fehler im Detail

Ende des Moduls

Danach können Sie

  • Einen Context Manager mit @contextmanager schreiben, mit try und finally um das yield
  • Mit suppress eine erwartete Exception ignorieren und mit closing close() aufrufen lassen
  • Eine Wiederholungsschleife bauen, die jeden Fehlschlag notiert und sammelt und aus der Gruppe einen klaren Fehler auslöst
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus der letzten Lektion: Wo erscheint die Notiz nach err.add_note("attempt 2")?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: ein Context Manager aus einer Funktion mit yield und ein Block, der scheitert. Was gibt das aus?

    from contextlib import contextmanager
    
    
    @contextmanager
    def step(name):
        print("start", name, end=" | ")
        yield
        print("end", name, end=" | ")
    
    
    try:
        with step("load"):
            raise OSError("disk")
    except OSError:
        print("caught")
  3. Üben · Aufgabe 3 von 7

    Zwei neue Schreibweisen in step oben: @contextmanager in der Zeile über def und yield darin. Welche Aussagen stimmen? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  4. Üben · Aufgabe 4 von 7

    Die Sperrdatei existiert nicht. Was gibt das aus?

    import os
    from contextlib import suppress
    
    with suppress(FileNotFoundError):
        os.remove("no-such.lock")
        print("removed", end=" ")
    print("done")
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Werkzeug zu, was es für Sie erledigt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. missing.txt existiert nicht. Was gibt das aus?

    from contextlib import contextmanager
    
    
    @contextmanager
    def attempt(n):
        try:
            yield
        except OSError:
            print("attempt", n, "failed", end=" | ")
    
    
    for n in [1, 2]:
        with attempt(n):
            open("missing.txt")
            print("read ok", end=" | ")
    print("after")
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie mit @contextmanager einen Context Manager temporarily_set(settings, key, value). Er setzt settings[key] = value für den with-Block und stellt danach den alten Wert wieder her, auch wenn der Block etwas auslöst. Gab es den Schlüssel vorher nicht, wird er wieder entfernt. Probieren Sie es mit einem Block, der etwas auslöst, und mit einem neuen Schlüssel.

    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

Ein Aufräumschritt mit allen drei Werkzeugen

step ist ein mit @contextmanager geschriebener Context Manager: Er hält Start, Fehlschlag und Ende in einem Bericht fest und löst den Fehler erneut aus. suppress darum herum ignoriert den FileNotFoundError, wenn old.lock schon weg ist, und closing ruft am Ende report.close() auf, weil Report zwar close() hat, aber selbst kein with unterstützt.

main.py

import os
from collections.abc import Iterator
from contextlib import closing, contextmanager, suppress


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

    def add(self, line: str) -> None:
        self.lines.append(line)

    def close(self) -> None:
        print(f"{self.name}: {len(self.lines)} lines, closed")


@contextmanager
def step(report: Report, name: str) -> Iterator[None]:
    report.add(f"start {name}")
    try:
        yield
    except OSError as err:
        report.add(f"failed {name}: {type(err).__name__}")
        raise
    finally:
        report.add(f"end {name}")


with closing(Report("cleanup")) as report:
    with suppress(FileNotFoundError):
        with step(report, "remove old.lock"):
            os.remove("old.lock")
    for line in report.lines:
        print(line)

Ausführen mit

python main.py

Ausgabe

start remove old.lock
failed remove old.lock: FileNotFoundError
end remove old.lock
cleanup: 3 lines, closed
  • step löst nach dem Protokollieren erneut aus, also erreicht der Fehler suppress, das ihn dann ignoriert.
  • finally fügt die end-Zeile hinzu, ob os.remove klappt oder scheitert.
  • closing(Report(...)) bindet den Bericht selbst an report und ruft nach der Schleife close() auf.
  • Die for-Schleife läuft innerhalb von closing, also wird der Bericht ausgegeben, bevor close() seine eigene Zeile schreibt.
Ä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 3

Schritt 1: ein Context Manager für einen Versuch

Beginnen Sie den Leser mit attempt(log, number), einem mit @contextmanager geschriebenen Context Manager. Er hängt beim Eintritt "attempt <n>: start" an und beim Verlassen "attempt <n>: end", auch wenn der Block scheitert. Löst der Block einen OSError aus, hängt er vor der end-Zeile "attempt <n>: <Typname>" an und löst den Fehler erneut aus. Andere Exceptions gehen unprotokolliert durch, bekommen aber trotzdem die end-Zeile.

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

    Setzen Sie das yield in ein try und die end-Zeile in eine finally-Klausel.

  2. Hinweis 2

    Dazwischen kommt except OSError as err:, das f"attempt {number}: {type(err).__name__}" anhängt.

  3. Hinweis 3

    Beenden Sie diesen except-Block mit einem einfachen raise, sonst wird der Fehler verschluckt.

Eine Lösung zeigen

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

from collections.abc import Iterator
from contextlib import contextmanager


@contextmanager
def attempt(log: list[str], number: int) -> Iterator[None]:
    log.append(f"attempt {number}: start")
    try:
        yield
    except OSError as err:
        log.append(f"attempt {number}: {type(err).__name__}")
        raise
    finally:
        log.append(f"attempt {number}: end")
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 collections.abc import Iterator
from contextlib import contextmanager


@contextmanager
def attempt(log: list[str], number: int) -> Iterator[None]:
    log.append(f"attempt {number}: start")
    yield
    log.append(f"attempt {number}: end")

test_main.py

from main import attempt


def test_success():
    """Ein funktionierender Block protokolliert start und end"""
    log = []
    with attempt(log, 1):
        pass
    assert log == ["attempt 1: start", "attempt 1: end"], f"das Protokoll ist {log!r}"


def test_os_error_logged_and_raised():
    """Ein OSError wird mit seinem Typ protokolliert und erneut ausgelöst"""
    log = []
    try:
        with attempt(log, 2):
            open("no_such_file.txt", encoding="utf-8")
    except FileNotFoundError:
        pass
    else:
        assert False, "der FileNotFoundError kam nicht beim Aufrufer an; lösen Sie ihn erneut aus"
    assert log == ["attempt 2: start", "attempt 2: FileNotFoundError", "attempt 2: end"], f"das Protokoll ist {log!r}"


def test_other_error_passes():
    """Andere Fehler gelten nicht als Fehlschlag, beenden den Versuch aber trotzdem"""
    log = []
    try:
        with attempt(log, 3):
            int("x")
    except ValueError:
        pass
    else:
        assert False, "der ValueError kam nicht beim Aufrufer an"
    assert log == ["attempt 3: start", "attempt 3: end"], f"das Protokoll ist {log!r}"

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 3

Schritt 2: wiederholen und sammeln

Schreiben Sie read_with_retry(path, attempts=3, log=None, wait=None). Versuchen Sie bis zu attempts-mal, jeweils in with attempt(log, n) mit n ab 1, die Datei zu öffnen und ihren Text zurückzugeben. Jeder OSError bekommt die Notiz "attempt <n>" und wird gesammelt; zwischen den Versuchen rufen Sie wait(n) auf, falls wait übergeben wurde. Scheitern alle, lösen Sie ReadFailedError(f"could not read {path} after {attempts} attempts") from ExceptionGroup("read errors", errors) aus. Schritt 1 steht in main.py. Der Typ Callable[[int], None] | None bedeutet: eine Funktion, die ein int nimmt, oder 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

    Schleife mit for n in range(1, attempts + 1):. Darin try: with attempt(log, n): und darin with open(...) as f: return f.read().

  2. Hinweis 2

    In except OSError as err: rufen Sie err.add_note(f"attempt {n}") auf, hängen err an und rufen wait(n) nur auf, wenn wait nicht None ist und n < attempts.

  3. Hinweis 3

    Nach der Schleife: raise ReadFailedError(...) from ExceptionGroup("read errors", errors). Setzen Sie vorher log = [], falls es None ist.

Eine Lösung zeigen

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

from collections.abc import Callable, Iterator
from contextlib import contextmanager


@contextmanager
def attempt(log: list[str], number: int) -> Iterator[None]:
    log.append(f"attempt {number}: start")
    try:
        yield
    except OSError as err:
        log.append(f"attempt {number}: {type(err).__name__}")
        raise
    finally:
        log.append(f"attempt {number}: end")


class ReadFailedError(Exception):
    """The file could not be read in any attempt."""


def read_with_retry(
    path: str,
    attempts: int = 3,
    log: list[str] | None = None,
    wait: Callable[[int], None] | None = None,
) -> str:
    if log is None:
        log = []
    errors: list[OSError] = []
    for n in range(1, attempts + 1):
        try:
            with attempt(log, n):
                with open(path, encoding="utf-8") as f:
                    return f.read()
        except OSError as err:
            err.add_note(f"attempt {n}")
            errors.append(err)
            if wait is not None and n < attempts:
                wait(n)
    raise ReadFailedError(f"could not read {path} after {attempts} attempts") from ExceptionGroup(
        "read errors", errors
    )
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 collections.abc import Callable, Iterator
from contextlib import contextmanager


@contextmanager
def attempt(log: list[str], number: int) -> Iterator[None]:
    log.append(f"attempt {number}: start")
    try:
        yield
    except OSError as err:
        log.append(f"attempt {number}: {type(err).__name__}")
        raise
    finally:
        log.append(f"attempt {number}: end")


class ReadFailedError(Exception):
    """The file could not be read in any attempt."""


def read_with_retry(
    path: str,
    attempts: int = 3,
    log: list[str] | None = None,
    wait: Callable[[int], None] | None = None,
) -> str:
    with open(path, encoding="utf-8") as f:
        return f.read()

test_main.py

import os

from main import ReadFailedError, read_with_retry


def test_reads_file():
    """Eine vorhandene Datei wird beim ersten Versuch gelesen"""
    with open("ready.txt", "w", encoding="utf-8") as f:
        f.write("hello")
    log = []
    got = read_with_retry("ready.txt", log=log)
    assert got == "hello", f"read_with_retry lieferte {got!r}"
    assert log == ["attempt 1: start", "attempt 1: end"], f"das Protokoll ist {log!r}"


def test_retries_until_file_appears():
    """Die Datei entsteht während der zweiten Wartezeit, Versuch 3 liest sie"""
    if os.path.exists("late.txt"):
        os.remove("late.txt")
    waits = []

    def wait(n):
        waits.append(n)
        if n == 2:
            with open("late.txt", "w", encoding="utf-8") as f:
                f.write("ready")

    log = []
    got = read_with_retry("late.txt", log=log, wait=wait)
    assert got == "ready", f"read_with_retry lieferte {got!r}"
    assert waits == [1, 2], f"wait wurde aufgerufen mit {waits!r}"
    assert log[-2:] == ["attempt 3: start", "attempt 3: end"], f"das Protokoll ist {log!r}"
    assert log.count("attempt 1: FileNotFoundError") == 1, f"das Protokoll ist {log!r}"


def test_gives_up():
    """Nach dem letzten Versuch: ReadFailedError aus einer Gruppe aller Fehler"""
    waits = []
    try:
        read_with_retry("never.txt", attempts=2, wait=waits.append)
    except ReadFailedError as err:
        assert str(err) == "could not read never.txt after 2 attempts", f"die Meldung ist {str(err)!r}"
        group = err.__cause__
        assert isinstance(group, ExceptionGroup), f"die Ursache ist {group!r}, erwartet: eine ExceptionGroup"
        notes = [getattr(e, "__notes__", []) for e in group.exceptions]
        assert notes == [["attempt 1"], ["attempt 2"]], f"die Notizen sind {notes!r}"
    else:
        assert False, "read_with_retry kehrte bei fehlender Datei normal zurück"
    assert waits == [1], f"wait wurde aufgerufen mit {waits!r}; nach dem letzten Versuch wird nicht gewartet"

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 3 von 3

Schritt 3: eine Sperrdatei, die immer verschwindet

Schreiben Sie read_locked(path, attempts=3, wait=None). Die Funktion legt die Sperrdatei path + ".lock" mit Modus "x" an, scheitert also mit FileExistsError, wenn ein anderer Leser die Sperre hält. Dann gibt sie read_with_retry(path, attempts, wait=wait) zurück. Was auch immer beim Lesen passiert, danach entfernt sie die Sperre; ist die Sperre schon weg, ist das in Ordnung. Schritte 1 und 2 stehen in main.py.

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

    Legen Sie die Sperre vor dem try an: Scheitert das, gibt es keine eigene Sperre zu entfernen.

  2. Hinweis 2

    Setzen Sie return read_with_retry(...) in ein try und entfernen Sie die Sperre in der finally-Klausel.

  3. Hinweis 3

    Schreiben Sie in finally with suppress(FileNotFoundError): os.remove(lock_path), dann ist eine schon entfernte Sperre in Ordnung.

Eine Lösung zeigen

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

import os
from collections.abc import Callable, Iterator
from contextlib import contextmanager, suppress


@contextmanager
def attempt(log: list[str], number: int) -> Iterator[None]:
    log.append(f"attempt {number}: start")
    try:
        yield
    except OSError as err:
        log.append(f"attempt {number}: {type(err).__name__}")
        raise
    finally:
        log.append(f"attempt {number}: end")


class ReadFailedError(Exception):
    """The file could not be read in any attempt."""


def read_with_retry(
    path: str,
    attempts: int = 3,
    log: list[str] | None = None,
    wait: Callable[[int], None] | None = None,
) -> str:
    if log is None:
        log = []
    errors: list[OSError] = []
    for n in range(1, attempts + 1):
        try:
            with attempt(log, n):
                with open(path, encoding="utf-8") as f:
                    return f.read()
        except OSError as err:
            err.add_note(f"attempt {n}")
            errors.append(err)
            if wait is not None and n < attempts:
                wait(n)
    raise ReadFailedError(f"could not read {path} after {attempts} attempts") from ExceptionGroup(
        "read errors", errors
    )


def read_locked(path: str, attempts: int = 3, wait: Callable[[int], None] | None = None) -> str:
    lock_path = path + ".lock"
    with open(lock_path, "x", encoding="utf-8"):
        pass
    try:
        return read_with_retry(path, attempts, wait=wait)
    finally:
        with suppress(FileNotFoundError):
            os.remove(lock_path)
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

import os
from collections.abc import Callable, Iterator
from contextlib import contextmanager, suppress


@contextmanager
def attempt(log: list[str], number: int) -> Iterator[None]:
    log.append(f"attempt {number}: start")
    try:
        yield
    except OSError as err:
        log.append(f"attempt {number}: {type(err).__name__}")
        raise
    finally:
        log.append(f"attempt {number}: end")


class ReadFailedError(Exception):
    """The file could not be read in any attempt."""


def read_with_retry(
    path: str,
    attempts: int = 3,
    log: list[str] | None = None,
    wait: Callable[[int], None] | None = None,
) -> str:
    if log is None:
        log = []
    errors: list[OSError] = []
    for n in range(1, attempts + 1):
        try:
            with attempt(log, n):
                with open(path, encoding="utf-8") as f:
                    return f.read()
        except OSError as err:
            err.add_note(f"attempt {n}")
            errors.append(err)
            if wait is not None and n < attempts:
                wait(n)
    raise ReadFailedError(f"could not read {path} after {attempts} attempts") from ExceptionGroup(
        "read errors", errors
    )


def read_locked(path: str, attempts: int = 3, wait: Callable[[int], None] | None = None) -> str:
    lock_path = path + ".lock"
    with open(lock_path, "x", encoding="utf-8"):
        pass
    text = read_with_retry(path, attempts, wait=wait)
    os.remove(lock_path)
    return text

test_main.py

import os

from main import ReadFailedError, read_locked


def clean(*names):
    for name in names:
        if os.path.exists(name):
            os.remove(name)


def test_success_removes_lock():
    """Nach erfolgreichem Lesen ist die Sperrdatei weg"""
    clean("a.txt.lock")
    with open("a.txt", "w", encoding="utf-8") as f:
        f.write("A")
    got = read_locked("a.txt")
    assert got == "A", f"read_locked lieferte {got!r}"
    assert not os.path.exists("a.txt.lock"), "a.txt.lock existiert nach erfolgreichem Lesen noch"


def test_failure_removes_lock():
    """Auch nach gescheitertem Lesen ist die Sperrdatei weg"""
    clean("b.txt", "b.txt.lock")
    try:
        read_locked("b.txt", attempts=2)
    except ReadFailedError:
        pass
    else:
        assert False, "read_locked kehrte bei fehlender Datei normal zurück"
    assert not os.path.exists("b.txt.lock"), "b.txt.lock existiert nach gescheitertem Lesen noch; entfernen Sie sie in finally"


def test_existing_lock_is_kept():
    """Existiert die Sperre, scheitert read_locked und lässt sie stehen"""
    with open("c.txt.lock", "w", encoding="utf-8") as f:
        f.write("held")
    try:
        read_locked("c.txt")
    except FileExistsError:
        pass
    else:
        assert False, "read_locked löste bei gehaltener Sperre keinen FileExistsError aus"
    assert os.path.exists("c.txt.lock"), "read_locked hat eine Sperre entfernt, die es nicht angelegt hat"
    clean("c.txt.lock")


def test_lock_already_gone():
    """Eine Sperre, die jemand anderes während des Lesens entfernt, ist kein Fehler"""
    clean("d.txt", "d.txt.lock")

    def wait(n):
        clean("d.txt.lock")
        with open("d.txt", "w", encoding="utf-8") as f:
            f.write("D")

    got = read_locked("d.txt", wait=wait)
    assert got == "D", f"read_locked lieferte {got!r}"

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

Aufräumen nach yield ohne finally

from contextlib import contextmanager


@contextmanager
def lock():
    print("lock")
    yield
    print("unlock")


with lock():
    raise OSError("disk full")

Was Python ausgibt

OSError: disk full

Warum, und die Lösung

Das Programm gibt lock aus, aber nie unlock: Der OSError kommt aus dem yield heraus und überspringt die Zeile danach. Setzen Sie das yield in try: und print("unlock") in finally:, dann läuft das Aufräumen, wie auch immer der Block endet.

@contextmanager vergessen

def lock():
    print("lock")
    yield
    print("unlock")


with lock():
    print("working")

Was Python ausgibt

TypeError: 'generator' object does not support the context manager protocol (missed __exit__ method)

Warum, und die Lösung

Ohne den Dekorator liefert lock() einen einfachen Generator, mit dem with nichts anfangen kann. Ergänzen Sie from contextlib import contextmanager und setzen Sie @contextmanager in die Zeile über def lock():.

Im Context Manager wiederholen

from contextlib import contextmanager


@contextmanager
def retrying(attempts):
    for n in range(attempts):
        try:
            yield
            return
        except OSError:
            print("attempt", n + 1, "failed")


with retrying(3):
    open("missing.txt")

Was Python ausgibt

RuntimeError: generator didn't stop after throw()

Warum, und die Lösung

Ein with-Block läuft einmal, also muss sein Generator genau einmal yield ausführen. Nach dem ersten Fehlschlag erneut yield ist ein Fehler. Setzen Sie die Wiederholungsschleife in eine Funktion, wie read_with_retry, und nutzen Sie einen Context Manager für einen einzelnen Versuch.

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

Gerade genug Syntax: @ und yield

@contextmanager in der Zeile über def ist ein Dekorator: Er umhüllt die Funktion darunter, sodass ihr Aufruf einen Context Manager für with liefert. Ohne ihn liefert der Aufruf nur ein Generator-Objekt, das with ablehnt. Eine Funktion mit yield darin ist eine Generatorfunktion. Hier muss sie genau einmal yield ausführen, und das yield markiert, wo der with-Block läuft: Die Funktion pausiert dort und macht weiter, wenn der Block endet. Mehr braucht diese Lektion nicht; Generatoren und Dekoratoren behandelt Modul I3 ausführlich.

@contextmanager: vor, am und nach dem yield

Der Code vor yield läuft beim Eintritt, der with-Block läuft am yield, der Code danach beim Verlassen. Ein mit yield übergebener Wert ist das, was as bindet. Löst der Block etwas aus, taucht die Exception am yield wieder auf; setzen Sie das yield also in try und das Aufräumen in finally. Ein except dort ohne erneutes raise verschluckt den Fehler: erst protokollieren, dann raise. Ein with-Block läuft einmal: genau ein yield. Die Annotation -> Iterator[None] sagt mypy, dass die Funktion yield nutzt; übernehmen Sie sie so, Typen folgen in Modul A2.

suppress und closing

with suppress(FileNotFoundError): os.remove(lock) entfernt eine Datei, die vielleicht schon weg ist. Tritt die Exception auf, wird der Rest des Blocks übersprungen, und das Programm läuft nach der with-Anweisung weiter; jede andere Exception geht trotzdem durch. Nennen Sie nur die Exception, die Sie erwarten. closing(thing) ist für Objekte mit einer close()-Methode, die selbst nicht in with nutzbar sind: Es ruft thing.close() auf, wenn der Block endet, wie auch immer er endet.

Der Build: wiederholen, sammeln, aufräumen

Der Leser versucht es bis zu attempts-mal. Jeder Versuch läuft in attempt(log, n), Ihrem Context Manager, der Start, Fehlschlag und Ende protokolliert. Ein gescheiterter OSError bekommt die Notiz "attempt n" und kommt in eine Liste, und zwischen den Versuchen ruft der Leser wait(n) auf, eine Funktion, die der Aufrufer übergibt: time.sleep im echten Einsatz, eine Attrappe in Tests. Scheitern alle Versuche, löst er ReadFailedError aus einer ExceptionGroup aller Fehler aus. Eine Sperrdatei markiert das Lesen, und finally entfernt sie mit suppress.

Quellen

Zuletzt geprüft am 29. September 2026