7. Exceptions
Exceptions
Abschnitt betitelt „Exceptions“Programme laufen nicht nur im Idealfall. Benutzer tippen Text, wo Zahlen erwartet werden. Dateien fehlen, sind schreibgeschützt oder haben das falsche Format. Netzwerke fallen aus. Ein professionelles Programm unterscheidet sich von einem Übungsskript vor allem darin, wie es mit solchen Situationen umgeht: Es stürzt nicht ab und rechnet nicht stillschweigend mit falschen Werten weiter, sondern erkennt den Fehler, reagiert angemessen und informiert verständlich.
Der Mechanismus dafür heißt in Python (wie in den meisten modernen Sprachen) Exception: ein Objekt, das einen Fehlerzustand beschreibt und den normalen Programmablauf unterbricht, bis es behandelt wird.
Warum nicht einfach Rückgabewerte?
Abschnitt betitelt „Warum nicht einfach Rückgabewerte?“Der naheliegende Ansatz zur Fehlermeldung sind spezielle Rückgabewerte:
def parse_frame_number(text): """Return the frame number, or -1 if the input is invalid.""" if not text.isdigit(): return -1 return int(text)Dieses Muster hat drei strukturelle Schwächen:
- Der Fehlerwert kollidiert mit echten Werten. Was, wenn -1 (oder 0, oder
None) irgendwann ein gültiges Ergebnis ist? Für jede Funktion muss ein eigener, unverwechselbarer Fehlerwert erfunden und dokumentiert werden. - Die Prüfung ist freiwillig. Vergisst der Aufrufer das
if result == -1, rechnet das Programm mit -1 als Framenummer weiter. Der Fehler fällt erst viel später auf, weit weg von seiner Ursache, oder gar nicht. - Fehlercodes müssen durchgereicht werden. Ruft
a()die Funktionb(), diec()ruft, und tritt der Fehler inc()auf, müssenb()unda()den Fehlerwert Ebene für Ebene weiterreichen. Der eigentliche Programmcode versinkt in Weiterleitungslogik.
Exceptions lösen alle drei Punkte: Sie sind vom normalen Rückgabeweg getrennt, sie lassen sich nicht versehentlich ignorieren (ein unbehandelter Fehler beendet das Programm sichtbar statt still falsch weiterzulaufen), und sie durchqueren beliebig viele Aufrufebenen von selbst, bis sich jemand zuständig erklärt.
Exceptions lesen: der Traceback
Abschnitt betitelt „Exceptions lesen: der Traceback“Unbehandelte Exceptions kennt jeder, der Python benutzt:
>>> int("abc")Traceback (most recent call last): File "<stdin>", line 1, in <module>ValueError: invalid literal for int() with base 10: 'abc'Der Traceback ist ein Untersuchungsbericht und wird von unten nach oben gelesen: Die letzte Zeile nennt den Typ des Fehlers (ValueError) und die Beschreibung. Die Zeilen darüber zeigen den Weg dorthin, also die Kette der Funktionsaufrufe von der äußersten Ebene bis zur Zeile, in der der Fehler auftrat. Bei tief verschachtelten Programmen ist die unterste Datei-und-Zeilen-Angabe der Ort des Geschehens.
Die Fähigkeit, einen Traceback ruhig zu lesen, statt ihn als Katastrophenmeldung zu überfliegen, ist eine Kernkompetenz der Fehlersuche.
try und except: Fehler behandeln
Abschnitt betitelt „try und except: Fehler behandeln“Mit try/except wird ein Codeabschnitt unter Beobachtung gestellt:
text = input("Frame number: ")
try: frames = int(text) print(f"frame {frames} selected")except ValueError: print(f"'{text}' is not a valid number.")Ablauf: Python führt den try-Block aus. Tritt dabei ein ValueError auf, springt die Ausführung sofort in den except-Block; der Rest des try-Blocks wird übersprungen. Tritt kein Fehler auf, wird der except-Block übersprungen. In beiden Fällen läuft das Programm danach normal weiter.
Gezielt fangen, nicht pauschal
Abschnitt betitelt „Gezielt fangen, nicht pauschal“Der except-Klausel wird der Exception-Typ mitgegeben, der behandelt werden soll. Mehrere Typen können getrennt behandelt werden:
values = {"width": "1920"}
try: width = int(values["heigth"]) # typo in the keyexcept KeyError as error: print(f"missing entry: {error}")except ValueError as error: print(f"not a number: {error}")Der Zusatz as error bindet das Exception-Objekt an einen Namen; seine Textdarstellung enthält die Detailinformation. Wichtig ist, was hier nicht steht:
# anti-pattern: silences every error, including bugstry: width = int(values["heigth"])except: passEin nacktes except: (oder except Exception:) fängt alles, auch Fehler, die mit dem erwarteten Problem nichts zu tun haben: Tippfehler im eigenen Code, fehlgeschlagene Importe, sogar den Programmabbruch mit Strg+C. Kombiniert mit pass entsteht die schlimmste Sorte Programm: eines, das Fehler verschluckt und mit unsinnigen Zuständen weiterläuft. Die Regel lautet daher: So spezifisch wie möglich fangen, und nur Fehler, mit denen an dieser Stelle sinnvoll umgegangen werden kann. Alles andere darf weiterfliegen; ein sichtbarer Absturz ist besser als ein unsichtbar falsches Ergebnis.
else und finally
Abschnitt betitelt „else und finally“Die vollständige Anweisung hat vier Teile:
try: frames = int(text)except ValueError: print("invalid input")else: print(f"frame {frames} selected") # runs only if NO exception occurredfinally: print("done") # runs ALWAYS, error or notelse nimmt den Erfolgsfall auf und hält den try-Block dadurch minimal: Nur die Zeile, die tatsächlich fehlschlagen kann, steht unter Beobachtung. finally läuft in jedem Fall, auch wenn die Exception nicht gefangen wurde, und ist für Aufräumarbeiten gedacht (Datei schließen, Verbindung trennen, temporäre Daten löschen). else und finally sind optional und deutlich seltener als das Grundpaar try/except.
Die Exception-Hierarchie
Abschnitt betitelt „Die Exception-Hierarchie“Exceptions sind Klassen, und sie bilden eine Vererbungshierarchie; die Konzepte aus Kapitel 5 gelten hier unmittelbar. Ein Ausschnitt der eingebauten Hierarchie:
classDiagram
BaseException <|-- KeyboardInterrupt
BaseException <|-- Exception
Exception <|-- ValueError
Exception <|-- TypeError
Exception <|-- LookupError
LookupError <|-- KeyError
LookupError <|-- IndexError
Exception <|-- OSError
OSError <|-- FileNotFoundError
OSError <|-- PermissionError
Exception <|-- ZeroDivisionError
Die für den Alltag wichtigsten Typen:
| Exception | tritt auf bei |
|---|---|
ValueError | richtiger Typ, unbrauchbarer Wert: int("abc") |
TypeError | falscher Typ: len(42) |
KeyError | fehlender Dictionary-Schlüssel |
IndexError | Listenindex außerhalb des Bereichs |
FileNotFoundError | Datei existiert nicht |
PermissionError | fehlende Zugriffsrechte |
ZeroDivisionError | Division durch 0 |
Ein except fängt den angegebenen Typ und alle seine Subklassen; das ist isinstance-Logik. except LookupError: behandelt also KeyError und IndexError gemeinsam, except OSError: alle Datei- und Systemfehler. Daran zeigt sich auch, warum except Exception: so grob ist: Es ist die Wurzel fast der gesamten Hierarchie.
raise: Exceptions selbst auslösen
Abschnitt betitelt „raise: Exceptions selbst auslösen“Eigener Code meldet Fehler mit raise. Das gehört überall dorthin, wo eine Funktion mit ungültigen Eingaben nichts Sinnvolles anfangen kann:
def frames_to_timecode(frames, fps=25): """Convert an absolute frame number to a timecode HH:MM:SS:FF.""" if frames < 0: raise ValueError(f"frame number must be >= 0, got {frames}") if fps <= 0: raise ValueError(f"fps must be positive, got {fps}") total_seconds = frames // fps return (f"{total_seconds // 3600:02d}:{(total_seconds % 3600) // 60:02d}:" f"{total_seconds % 60:02d}:{frames % fps:02d}")Damit ändert die Funktion ihren Charakter: Statt bei negativem Input stillschweigend einen absurden Timecode zu berechnen, verweigert sie die Arbeit laut und mit Begründung. Der Fehlertext nennt die Erwartung und den tatsächlichen Wert; wer den Traceback liest, weiß sofort, was schiefging. Solche frühen, präzisen Prüfungen an den Grenzen einer Funktion nennt man Eingangsvalidierung; sie verwandeln späte, rätselhafte Folgefehler in frühe, klare Meldungen.
Auch die Properties aus Kapitel 4 bekommen damit ihre saubere Form:
class Account: # ...
@balance.setter def balance(self, new_balance): if new_balance < 0: raise ValueError("balance must not be negative") self._balance = new_balanceDas provisorische print von damals ist ersetzt: Der Setter erzwingt die Regel jetzt, statt nur an sie zu erinnern.
Eigene Exception-Klassen
Abschnitt betitelt „Eigene Exception-Klassen“Die eingebauten Typen decken technische Fehler ab. Für fachliche Fehler definiert man eigene Exception-Klassen, und zwar durch Vererbung von Exception:
class InsufficientFundsError(Exception): """Raised when a withdrawal exceeds the account balance."""
class Account: def __init__(self, owner, balance=0): self.owner = owner self._balance = balance
def withdraw(self, amount): if amount > self._balance: raise InsufficientFundsError( f"withdrawal of {amount} exceeds balance of {self._balance}" ) self._balance -= amountDer Klassenkörper bleibt meist leer (der Docstring genügt); der Wert liegt im Namen. Der Aufrufer kann fachliche Fehler jetzt getrennt von technischen behandeln:
try: account.withdraw(500)except InsufficientFundsError: print("Not enough money in the account.")Für größere Programme lohnt eine kleine eigene Hierarchie, etwa eine gemeinsame Basisklasse pro Modul:
class MediaLibraryError(Exception): """Base class for all media library errors."""
class UnknownFormatError(MediaLibraryError): pass
class CorruptFileError(MediaLibraryError): passWer alles aus der Mediathek fangen will, schreibt except MediaLibraryError:; wer nur kaputte Dateien behandelt, fängt CorruptFileError. Das ist Vererbung und Polymorphismus, angewendet auf Fehlerbehandlung.
Robuste Benutzereingaben
Abschnitt betitelt „Robuste Benutzereingaben“Der Lehrplan verlangt ausdrücklich das Abfangen von Eingabefehlern. Das Standardmuster dafür ist die Validierungsschleife: fragen, prüfen, bei Fehlern erklären und erneut fragen:
def ask_int(prompt, minimum, maximum): """Ask until the user enters an integer within [minimum, maximum].""" while True: text = input(prompt) try: value = int(text) except ValueError: print(f"'{text}' is not a whole number. Please try again.") continue if not minimum <= value <= maximum: print(f"Please enter a value between {minimum} and {maximum}.") continue return value
age = ask_int("Your age: ", 10, 120)Beachtenswert an dieser Funktion:
- Die Schleife endet ausschließlich über das
returnim Erfolgsfall. Der Benutzer kommt erst weiter, wenn die Eingabe gültig ist, und das Programm dahinter kann sich auf den Wert verlassen. - Die Fehlermeldungen sind für Menschen formuliert: Sie sagen, was falsch war und was stattdessen erwartet wird.
"Error: invalid input"erfüllt keines der beiden Kriterien. Für ein Publikum, das später Benutzeroberflächen gestaltet, gilt: Fehlermeldungen sind Teil des Interfaces, nicht technisches Beiwerk. - Die Funktion ist wiederverwendbar für jede ganzzahlige Eingabe mit Bereichsgrenzen. Solche Eingabehilfen sind der Grundstock jeder Konsolenanwendung und kehren bei der GUI-Entwicklung (Kapitel 14) in Form von Feldvalidierung wieder.
Strategie: Wo fangen, wo weiterreichen?
Abschnitt betitelt „Strategie: Wo fangen, wo weiterreichen?“Die schwierigste Frage der Fehlerbehandlung ist nicht wie, sondern wo. Die Antwort ergibt sich aus einer einzigen Überlegung: Gefangen wird dort, wo eine sinnvolle Reaktion möglich ist.
- Eine Hilfsfunktion tief im Programm kann meist nicht sinnvoll reagieren. Sie weiß nicht, ob sie in einem Konsolenprogramm, einer GUI oder einem Test läuft; ein
printan dieser Stelle wäre eine Anmaßung. Sie soll den Fehler weiterfliegen lassen (oder mitraiseeinen fachlich präziseren auslösen). - Die äußere Ebene, etwa das Hauptprogramm oder der Klick-Handler einer GUI, kennt den Kontext: Sie kann den Benutzer informieren, den Vorgang abbrechen, einen neuen Versuch anbieten oder den Fehler protokollieren. Dort gehört das
excepthin.
Als Konsequenz enthalten gut strukturierte Programme deutlich weniger try-Blöcke, als Anfänger vermuten: viele Funktionen, die Fehler nur auslösen oder passieren lassen, und wenige, zentrale Stellen, die sie behandeln.
Lernergebnisse: Was Sie nach diesem Kapitel können sollten
Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:
- Erklären: warum Exceptions Fehlercodes überlegen sind und wie ein Traceback gelesen wird.
- Anwenden: Fehler mit
try/except(inklusiveelseundfinally) gezielt fangen und typspezifisch behandeln. - Nennen: die wichtigsten eingebauten Exception-Typen und ihre Stellung in der Vererbungshierarchie nennen.
- Anwenden: Eingaben an Funktionsgrenzen validieren und mit
raisepräzise Fehler auslösen. - Entwerfen: eigene Exception-Klassen (auch als kleine Hierarchie) definieren und einsetzen.
- Anwenden: Benutzereingaben mit Validierungsschleifen robust machen und Fehlermeldungen benutzergerecht formulieren.
- Beurteilen: entscheiden, auf welcher Programmebene ein Fehler behandelt und wo er weitergereicht wird.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 14 - Robust gegen alles