Aufwärmen · Aufgabe 1 von 7
// 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
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
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")Ü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.
Ü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")Üben · Aufgabe 5 von 7
Ordnen Sie jedem Werkzeug zu, was es für Sie erledigt.
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")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.pyAusgabe
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
Hinweis 1
Setzen Sie das yield in ein try und die end-Zeile in eine finally-Klausel.
Hinweis 2
Dazwischen kommt except OSError as err:, das f"attempt {number}: {type(err).__name__}" anhängt.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
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().
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.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
Legen Sie die Sperre vor dem try an: Scheitert das, gibt es keine eigene Sperre zu entfernen.
Hinweis 2
Setzen Sie return read_with_retry(...) in ein try und entfernen Sie die Sperre in der finally-Klausel.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHä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 fullWarum, 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.