Exceptions — was tun, wenn etwas bricht
Einen bestimmten Fehler mit try/except fangen, warum `except Exception: pass` fast immer falsch ist, `else` und `finally`, eigene Fehler auslösen, und den Fehler in der richtigen Größe aus der Familie auswählen.
Wie möchtest du lernen?
Das Problem, das wir lösen
Die letzte Frage des vorigen Kapitels ließ ein Programm drei Zeilen korrekt verarbeiten und bei der vierten stoppen. Kleiner sieht es so aus:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(Vor der Fehlerzeile druckt Python auch einige Traceback-Zeilen; in diesem Kapitel wird meist nur die letzte Zeile gezeigt.)
10 wurde korrekt addiert. 30 wurde nie addiert. Und print(total) wurde nie ausgeführt.
Eine schlechte Zeile hat die ganze Arbeit gestoppt. Bis jetzt hatten wir nur zwei Möglichkeiten — entweder sind alle Daten perfekt, oder das Programm stoppt.
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40Es gibt eine dritte Möglichkeit: das Problem geschehen lassen und dann darauf reagieren.
Am Ende dieses Kapitels kannst du
- Einen bestimmten Fehler mit
try/exceptfangen - Sagen, warum ein blankes
except:oderexcept Exception: passfast immer falsch ist - Den Fehler mit
as errin die Hand nehmen und seine Meldung lesen - Sagen, was
elseundfinallytun - Deine eigenen Fehler mit
raiseauslösen und einschätzen, wann - Die Familie der Fehler erkennen und einen in der richtigen Größe fangen
Voraussetzungen: Dateien lesen und schreiben.
Ein Fehler ist auch ein Objekt
as zu schreiben legt den Fehler in deine Hände:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'Der rote Text, den du im Terminal die ganze Zeit gesehen hast, sind diese Objekte — und eines zu drucken gibt seine Meldung.
Daraus folgt etwas Praktisches: der Fehler trägt die beste Erklärung, die es gibt. err zu drucken ist fast immer nützlicher, als mit eigenen Worten „etwas ist schiefgegangen“ zu schreiben.
Den richtigen Fehler fangen
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'Ein except fängt nur die Art, die es nennt. Dieses nennt TypeError, und passiert ist ein ValueError — der except-Block ist also so gut wie nicht da.
Das kann sich hinderlich anfühlen, und genau das ist der nützliche Teil. Die except-Zeile sagt, für welchen Fehler du gewappnet warst — und alles, wofür du nicht gewappnet warst, stoppt das Programm weiterhin, und das ist richtig.
Deshalb solltest du das nicht schreiben:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40Die Antwort stimmt, und der Code ist trotzdem schlecht. except Exception: pass verschluckt alles — die Fehler, über die du nachgedacht hast, und die, über die du nicht nachgedacht hast. Wenn numbers morgen aus Versehen zu einem Dictionary wird oder ein Name darin falsch geschrieben ist, liefert dieses Programm stillschweigend eine falsche Antwort.
Die Regel: Fang genau den Fehler, für den du einen Plan hast. Und wenn du ihn gefangen hast, tu etwas — sag mindestens, was passiert ist.
Mehr als eine Art
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionMehrere except-Blöcke können aufeinanderfolgen, und der erste, der passt, läuft — die übrigen werden gar nicht erst angesehen.
Wenn die Antwort dieselbe ist, kann man sie zusammen schreiben:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'type(err).__name__ gibt den Namen der Fehlerart — nützlich, wenn man Logs schreibt.
Fehler kommen in Familien
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
TrueFehler sind als Baum angeordnet, und eine Elternklasse zu fangen, fängt auch ihre Kinder:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneEin paar Beziehungen, die sich zu wissen lohnen: FileNotFoundError und PermissionError sind beide OSError; KeyError und IndexError sind beide LookupError; und fast alles ist eine Exception.
Daraus folgt die Regel: Fang den kleinsten Fehler, den du kannst. except Exception ist am leichtesten zu schreiben und am wenigsten nützlich.
else und finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Noneelse läuft, wenn nichts im try-Block kaputtgegangen ist. So bleibt das Innere von try klein — nur die Zeile, die scheitern kann — und die Arbeit nach dem Erfolg kommt in else. Steckst du zu viel hinein, fängst du Dinge, die du nie fangen wolltest.
finally läuft immer, was auch immer passiert ist. Auch nach einem return:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryDas return hat den Wert festgelegt, aber finally läuft, bevor die Funktion verlassen wird. Es ist der Ort zum Aufräumen — wobei with bei Dateien diese Arbeit längst erledigt hat.
Deine eigenen auslösen
Etwas auszulösen zählt genauso viel wie es zu fangen.
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1Was wäre die Alternative zu raise gewesen? None oder 0 zurückgeben — und dann wäre der Fehler weitergereicht worden. Er hätte sich in irgendeine Summe hineingearbeitet und wäre viel später als eine seltsame Zahl ohne nachvollziehbaren Ursprung aufgetaucht.
Ein Fehler stoppt den Bug dort, wo er entstanden ist, und sagt in seiner Meldung, warum.
Wenn du diese Meldung schreibst, steck den betroffenen Wert hinein. "invalid quantity" und "quantity cannot be negative: -1" — nur die zweite lässt dich mit der Arbeit anfangen.
Einen Fehler kann man auch fangen und mit einer besseren Meldung neu auslösen. parse.py:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'Diesmal wird der ganze Output gezeigt, weil hier die letzte Zeile nicht die ganze Geschichte erzählt. (Auf deinem Rechner enthalten die File-Zeilen den vollständigen Pfad zur Datei; hier ist er auf ... gekürzt.) Python hat zwei Tracebacks gedruckt: der erste ist der ursprüngliche Fehler aus int(), der zweite ist unser neuer ValueError. Die Zeile in der Mitte, During handling of the above exception, another exception occurred:, sagt, dass der zweite während der Behandlung des ersten passiert ist. Keine Information geht verloren — aber es liest sich, als gäbe es einen Bug im except-Block.
Wenn du absichtlich einen neuen Fehler auslöst, sag das auch: Schreib except ValueError as err: und dann raise ValueError(f"not a number: {text!r}") from err. Die Mittelzeile wird dann zu The above exception was the direct cause of the following exception: — also „der erste hat den zweiten verursacht“, und genau das meinen wir.
{text!r} setzt den Wert über repr ein, sodass die Anführungszeichen sichtbar sind — und der Unterschied zwischen not a number: 'ten' und not a number: ten zählt, wenn der Wert wirklich ein leerer String oder ein überzähliges Leerzeichen ist.
Erst fragen, oder sich später entschuldigen
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedBeide sind richtig. Die zweite Variante ist in Python üblicher, und mit gutem Grund: die erste schaut zweimal nach — einmal mit in, einmal mit [...] — und kann trotzdem brechen, wenn sich dazwischen etwas ändert.
Für das, was routinemäßig passiert, liest sich ein if aber besser. Ausnahmen sind für das Ausnahmehafte; der Name sagt es.
Ein vollständiges Beispiel
orders.txt, mit zwei absichtlich schlechten Zeilen:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py:
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2Vier Dinge, die sich lohnt anzusehen.
parse_line wirft und read_order fängt. Die untere Funktion hat keine Ahnung, was das Programm insgesamt tun soll — sie weiß nur, dass diese Zeile unbenutzbar ist. Zu entscheiden, was damit zu tun ist, gehört zur Funktion darüber. Fehler werden unten ausgelöst und oben gefangen, und diese Aufteilung ist die ganze Aufgabe eines Exception-Systems.
Die Zeilennummer steht in der Meldung. could not convert string to float: 'eight' sagt nicht, wo du suchen sollst; line 2: ... schon. Alles, was du im Moment des Fangens weißt und später nicht mehr weißt, gehört in die Meldung.
read_order bricht nie. Es gibt zwei Dinge zurück — was benutzbar war und was nicht. Eine fehlende Datei liefert eine Antwort in derselben Form, also braucht main nicht zwei getrennte Wege.
Der innere try-Block ist genau eine Zeile lang. float(price) und int(quantity) — nur der Teil, der scheitern kann, sitzt darin. parts = raw.split(",") ist draußen, weil er nicht scheitern soll, und falls doch, will ich es wissen.
Wenn es schiefgeht
Der Fehler wird nicht gefangen, obwohl ein except da ist Der Typ passt nicht. Das erste Wort der letzten Zeile der Meldung ist der Name des Fehlers — nenne genau diesen, oder fange seine Elternklasse.
Es gibt ein except und das Programm stoppt trotzdem Die Zeile, die scheitert, sitzt nicht im try-Block. Prüfe die Einrückung.
SyntaxError: expected 'except' or 'finally' block Ein try wurde geschrieben, ohne except. try kann nicht allein stehen.
Alles „funktioniert“, aber die Antworten stimmen nicht Irgendwo gibt es ein except Exception: pass. Such es — dort ist das Problem.
Ich habe etwas ausgelöst, aber es erscheint keine Meldung Es wurde raise ValueError geschrieben statt raise ValueError("..."). Ergänze die Klammern und die Meldung.
Ein return in finally verliert den Fehler Ja, und deshalb solltest du dort keines schreiben. finally ist nur zum Aufräumen da.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Es gibt ein except. Was passiert mit dem Programm?
try:
int("twenty")
except TypeError:
print("caught")- A`caught` wird gedruckt
- BEs wird nichts gedruckt, und das Programm endet still
- CEs stoppt mit einem `TypeError`
- DDas Programm stoppt mit einem `ValueError` — das `except` ist also so gut wie nicht da
Die ganze Schleife ist im try. Was wird gedruckt?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
Sowohl try als auch except haben ein return. Was wird gedruckt?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Geh zurück zum summary.py des vorigen Kapitels, das people.txt gelesen hat.
Mach es jetzt so, dass es nie abstürzt und am Ende sagt, welche Zeilen übersprungen wurden und warum. Schreib eine parse_line(raw, number), die für eine schlechte Zeile einen ValueError mit der Zeilennummer darin auslöst.
Dann beschädige die Datei absichtlich — entferne ein Komma aus einer Zeile, schreibe in einer anderen Buchstaben an die Stelle, wo eine Zahl hingehört, und lass eine Zeile ganz leer. Zum Beispiel:
rafi,12
nadia 30
sami,ten
lina,25Die leere Zeile ist keine Beschwerde — wie im vorigen Kapitel wird sie stillschweigend übersprungen. Das Programm sollte also bis zum Ende laufen und zwei Beschwerden melden. Wenn du die Meldungen so schreibst wie im vollständigen Beispiel (expected 2 fields, got ... und line {number}: {err}), sieht das Ende des Outputs genau so aus:
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'Dann vier Experimente:
- Ersetze
except ValueErrordurchexcept Exception: pass. Was ist der Output, und warum ist er gefährlicher? - Entferne die Zeilennummer aus der Meldung von
parse_line. Wie lange dauert es jetzt, eine schlechte Zeile zu finden? - Mach den
try-Block größer — stecke den ganzen Körper der Schleife hinein. Was passiert jetzt, wenn eine Zeile schlecht ist? - Ergänze ein
finally, das nach jeder Zeile etwas druckt, und prüfe, dass es bei den guten wie bei den schlechten läuft.
Das dritte Experiment lehrt die nützlichste Gewohnheit dieses Kapitels: je kleiner der try-Block, desto ehrlicher der Fang.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen