8. Fehler und Ausnahmen¶
Bis jetzt wurden Fehlermeldungen nur erwähnt, aber wenn du die Beispiele ausprobiert hast, hast du wahrscheinlich einige gesehen. Es gibt (mindestens) zwei Arten von Fehlern, die man unterscheiden kann: Syntaxfehler und Ausnahmen.
8.1. Syntax Fehler¶
Syntaxfehler, auch bekannt als Parsing-Fehler, sind vielleicht die häufigste Art von Beschwerden, die man bekommt, wenn man Python noch lernt:
>>> while True print('Hello world')
Datei "<stdin>", Zeile 1
while True print('Hello world')
^^^^^
SyntaxError: ungültige Syntax
Der Parser wiederholt die fehlerhafte Zeile und zeigt kleine Pfeile an, die auf die Stelle verweisen, an der der Fehler erkannt wurde. Beachte, dass dies nicht immer die Stelle ist, die korrigiert werden muss. Im Beispiel wird der Fehler bei der Funktions print() erkannt, da unmittelbar davor ein Doppelpunkt (':') fehlt.
Der Dateiname (in unserem Beispiel <stdin> ) und die Zeilennummer werden angezeigt, damit du weisst, wo du nachsehen musst, falls die Eingabe aus einer Datei stammt.
8.2. Ausnahmen¶
Auch wenn eine Anweisung oder ein Ausdruck syntaktisch korrekt ist, kann es zu einem Fehler kommen, wenn man versucht, sie auszuführen. Fehler, die während der Ausführung entdeckt werden, werden Ausnahmen genannt und sind nicht unbedingt fatal: Du wirst bald lernen, wie man sie in Python-Programmen behandelt. Die meisten Ausnahmen werden jedoch nicht von Programmen behandelt und führen zu Fehlermeldungen wie hier gezeigt:
>>> 10 * (1/0)
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: Division durch Null
>>> 4 + spam*3
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 1, in <module>
4 + spam*3
^^^^
NameError: Der Name 'spam' ist nicht definiert
>>> '2' + 2
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 1, in <module>
'2' + 2
~~~~^~~
TypeError: Es können nur Zeichenketten (nicht "int") mit Zeichenketten verkettet werden
In der letzten Zeile der Fehlermeldung steht, was passiert ist. Ausnahmen gibt es in verschiedenen Typen, und der Typ wird als Teil der Meldung ausgegeben: Die Typen im Beispiel sind ZeroDivisionError, NameError und TypeError. Die als Ausnahmetyp ausgegebene Zeichenkette ist der Name der aufgetretenen eingebauten Ausnahme. Dies gilt für alle eingebauten Ausnahmen, muss aber nicht für benutzerdefinierte Ausnahmen gelten (obwohl es eine nützliche Konvention ist). Standardausnahmenamen sind eingebaute Bezeichner (keine reservierten Schlüsselwörter).
Der Rest der Zeile enthält detaillierte Informationen über die Art der Ausnahme und deren Ursache.
Der vorangehende Teil der Fehlermeldung zeigt den Kontext, in dem die Ausnahme aufgetreten ist, in Form eines Stack-Tracebacks. Im Allgemeinen enthält er einen Stack-Traceback, der die Zeilen des Quellcodes auflistet; er zeigt jedoch keine Zeilen an, die von der Standardeingabe gelesen wurden.
Eingebaute Exceptions listet die eingebauten Ausnahmen und ihre Bedeutungen auf.
8.3. Behandlung von Ausnahmen¶
Es ist möglich, Programme zu schreiben, die ausgewählte Ausnahmen behandeln. Schau dir das folgende Beispiel an, das den Benutzer zur Eingabe auffordert, bis ein gültiger Integer eingegeben wurde, dem Benutzer aber erlaubt, das Programm zu unterbrechen (mit Control-C oder was auch immer das Betriebssystem unterstützt); beachte, dass eine vom Benutzer erzeugte Unterbrechung durch das Auslösen der Ausnahme KeyboardInterrupt signalisiert wird.
>>> while True:
... try:
... x = int(input("Bitte gib eine Zahl ein: "))
... break
... except ValueError:
... print("Hoppla! Das war keine gültige Zahl. Versuche es noch einmal...")
...
Die try Anweisung funktioniert wie folgt.
Zuerst wird die Try-Klausel (Die Anweisung(en) zwischen den Schlüsselwörtern
tryundexcept) ausgeführt.Wenn keine Ausnahme auftritt, wird die except clause übersprungen und die Ausführung von der
tryAnweisung beendet.Wenn während der Ausführung der
tryKlausel eine Ausnahme auftritt, wird der Rest der Klausel übersprungen. Wenn der Typ der Ausnahme mit der nach demexceptSchlüsselwort genannten Ausnahme übereinstimmt, wird die Except-Klausel ausgeführt, und die Ausführung wird nach dem try/except-Block fortgesetzt.Tritt eine Ausnahme auf, die nicht mit der in der Except-Klausel genannten Ausnahme übereinstimmt, wird sie an äußere
tryAnweisungen weitergegeben; wird kein Handler gefunden, handelt es sich um eine unbehandelte Ausnahme und die Ausführung wird mit einer Fehlermeldung beendet.
A try statement may have more than one except clause, to specify
handlers for different exceptions. At most one handler will be executed.
Handlers only handle exceptions that occur in the corresponding try clause,
not in other handlers of the same try statement. An except clause
may name multiple exceptions as a parenthesized tuple, for example:
... except (RuntimeError, TypeError, NameError):
... pass
Eine Klasse in einer except -Klausel deckt Ausnahmen ab, bei denen es sich um Instanzen dieser Klasse selbst oder einer ihrer abgeleiteten Klassen handelt (jedoch nicht umgekehrt – eine except“-Klausel, in der eine abgeleitete Klasse aufgeführt ist, deckt keine Instanzen ihrer Basisklassen ab). Der folgende Code gibt beispielsweise B, C und D in dieser Reihenfolge aus:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
Beachte, dass, wenn die Exception-Klauseln umgekehrt wären (mit except B zuerst), B, B, B ausgegeben worden wäre — die erste passende Exception-Klausel wird ausgelöst.
Wenn eine Ausnahme auftritt, kann sie mit Werten verbunden sein, die auch als Argumente der Ausnahme bezeichnet werden. Das Vorhandensein und die Art der Argumente hängen von der Art der Ausnahme ab.
In der Except-Klausel kann nach dem Namen der Ausnahme eine Variable angegeben werden. Die Variable ist an die Ausnahmen-Instanz gebunden, die typischerweise ein args Attribut hat, das die Argumente speichert. Der Einfachheit halber definieren eingebaute Ausnahmetypen __str__(), um alle Argumente auszugeben, ohne explizit auf .args zuzugreifen.
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # der Exception-Typ
... print(inst.args) # in .args gespeicherte Argumentes
... print(inst) # __str__ erlaubt die direkte Ausgabe von args,
... # kann aber in Exception-Unterklassen überschrieben werden
... x, y = inst.args # args entpacken
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
Bei unbehandelten Ausnahmen wird die __str__() Ausgabe der Ausnahme als letzter Teil („Detail“) der Meldung ausgegeben.
BaseException ist die gemeinsame Basisklasse für alle Ausnahmen. Eine ihrer Unterklassen, Exception, ist die Basisklasse für alle nicht schwerwiegenden Ausnahmen. Ausnahmen, die keine Unterklassen von Exception sind, werden in der Regel nicht behandelt, da sie dazu dienen, anzuzeigen, dass das Programm beendet werden soll. Dazu gehört die SystemExit Ausnahme, die von sys.exit() und KeyboardInterrupt ausgelöst wird, wenn ein Benutzer das Programm unterbrechen möchte.
Exception kann als Platzhalter verwendet werden, der (fast) alles abfängt. Es empfiehlt sich jedoch, die Arten von Ausnahmen, die behandelt werden sollen, so genau wie möglich festzulegen und zuzulassen, dass unerwartete Ausnahmen weitergegeben werden.
Das gängigste Muster für die Exception Behandlung ist, die Ausnahme auszugeben oder zu protokollieren und sie dann erneut auszulösen (damit ein Aufrufer die Ausnahme ebenfalls behandeln kann):
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS-Fehler:", err)
except ValueError:
print("Daten konnten nicht in eine Ganzzahl konvertiert werden.")
except Exception as err:
print(f"Unerwarteter {err=}, {type(err)=}")
raise
Die try … except Anweisung hat eine optionale Else-Klausel, die, wenn vorhanden, allen Except-Klauseln folgen muss. Sie ist nützlich für Code, der ausgeführt werden muss, wenn die Try-Klausel keine Ausnahme auslöst. Zum Beispiel:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('kann nicht geöffnet werden', arg)
else:
print(arg, 'hat', len(f.readlines()), 'Zeilen')
f.close()
Die Verwendung der else Klausel ist besser als das Hinzufügen von zusätzlichem Code zu der try Klausel, weil dadurch vermieden wird, dass versehentlich eine Ausnahme abgefangen wird, die nicht durch den Code ausgelöst wurde, der durch die try … except Anweisung geschützt wird.
Ausnahmehandler behandeln nicht nur Ausnahmen, die unmittelbar in der Try-Klausel auftreten, sondern auch solche, die innerhalb von Funktionen auftreten, die (auch indirekt) in der Try-Klausel aufgerufen werden. Zum Beispiel:
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('Behandlung eines Laufzeitfehlers:', err)
...
Behandlung eines Laufzeitfehlers: Division durch Null
8.4. Auslösen von Ausnahmen¶
Die raise Anweisung ermöglicht es dem Programmierer, das Auftreten einer bestimmten Ausnahme zu erzwingen. Zum Beispiel:
>>> raise NameError('HiThere')
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 1, in <module>
raise NameError('HiThere')
NameError: HiThere
Das reine raise Argument gibt die Ausnahme an, die ausgelöst werden soll. Dabei muss es sich entweder um eine Ausnahme-Instanz oder eine Ausnahmen-Klasse handeln (Eine Klasse, die von einer BaseException, wie Exception oder einer ihrer Unterklassen abgeleitet ist). Wenn eine Ausnahmeklasse übergeben wird, wird sie implizit instanziiert, indem ihr Konstruktor ohne Argumente aufgerufen wird:
raise ValueError # Kurzform für 'raise ValueError()'
Wenn du feststellen musst, ob eine Ausnahme ausgelöst wurde, aber nicht beabsichtigst, diese zu behandeln, kannst du mit einer einfacheren Form der raise Anweisung die Ausnahme erneut auslösen:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('Eine Ausnahme ist aufgetreten!')
... raise
...
Eine Ausnahme ist aufgetreten!
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 2, in <module>
raise NameError('HiThere')
NameError: HiThere
8.5. Verkettung von Ausnahmen¶
Tritt eine unbehandelte Ausnahme innerhalb eines except Abschnitts auf, wird die behandelte Ausnahme an diesen Abschnitt angehängt und in die Fehlermeldung aufgenommen:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("database.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error
Um anzuzeigen, dass eine Ausnahme eine direkte Folge einer anderen ist, erlaubt die raise Anweisung eine optionale from Klausel:
# exc muss eine Ausnahminstanz oder None sein.
raise RuntimeError from exc
Dies kann nützlich sein, wenn du Ausnahmen umwandeln wikkst. Zum Beispiel:
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
func()
~~~~^^
File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database
Es erlaubt auch die Deaktivierung der automatischen Ausnahmeverkettung mit dem from None Idiom:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 4, in <module>
raise RuntimeError from None
RuntimeError
Weitere Informationen zur Verkettungsmechanik findest du unter Eingebaute Exceptions.
8.6. Benutzerdefinierte Ausnahmen¶
Programme können ihre eigenen Ausnahmen benennen, indem sie eine neue Ausnahmeklasse erstellen (siehe Klassen für mehr über Python-Klassen). Ausnahmen sollten in der Regel von der Exception Klasse abgeleitet werden, entweder direkt oder indirekt.
Es können Ausnahmeklassen definiert werden, die alles tun können, was jede andere Klasse auch tun kann, aber sie sind in der Regel einfach gehalten und bieten oft nur eine Reihe von Attributen, die es ermöglichen, Informationen über den Fehler durch Handler für die Ausnahme zu extrahieren.
Die meisten Ausnahmen werden mit Namen definiert, die auf „Error“ enden, ähnlich der Benennung der Standardausnahmen.
Viele Standardmodule definieren ihre eigenen Ausnahmen, um Fehler zu melden, die in den von ihnen definierten Funktionen auftreten können.
8.7. Definition von Bereinigungsaktionen¶
Die try Anweisung enthält eine weitere optionale Klausel, die dazu dient, Bereinigungsaktionen zu definieren, die unter allen Umständen ausgeführt werden müssen. Zum Beispiel:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Auf Wiedersehen, Welt!')
...
Auf Wiedersehen, Welt!
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
Wenn eine finally Klausel vorhanden ist, wird die finally Klausel als letzte Aufgabe ausgeführt, bevor die try Anweisung abgeschlossen wird. Die finally Klausel wird unabhängig davon ausgeführt, ob die try Anweisung eine Ausnahme erzeugt oder nicht. In den folgenden Punkten werden komplexere Fälle beim Auftreten einer Ausnahme behandelt:
Wenn während der Ausführung der
tryKlausel eine Ausnahme auftritt, kann die Ausnahme von einerexceptKlausel behandelt werden. Wenn die Ausnahme nicht durch eineexceptKlausel behandelt wird, wird die Ausnahme erneut ausgelöst, nachdem diefinallyKlausel ausgeführt wurde.Während der Ausführung einer
exceptoderelseKlausel kann eine Ausnahme auftreten. Auch hier wird die Ausnahme erneut ausgelöst, nachdem diefinallyKlausel ausgeführt wurde.If the
finallyclause executes abreak,continueorreturnstatement, exceptions are not re-raised.Wenn die
tryAnweisung einebreak,continueoderreturnAnweisung erreicht, wird diefinallyKlausel direkt vor der Ausführung derbreak,continueoderreturnAnweisung ausgeführt.If a
finallyclause includes areturnstatement, the returned value will be the one from thefinallyclause’sreturnstatement, not the value from thetryclause’sreturnstatement.
Zum Beispiel:
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
Ein komplizierteres Beispiel:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("Division durch Null!")
... else:
... print("Ergebnis ist", result)
... finally:
... print("Ausführung der finally-Klausel")
...
>>> divide(2, 1)
Ergebnis ist 2.0
Ausführung der finally-Klausel
>>> divide(2, 0)
Division durch Null!
Ausführung der finally-Klausel
>>> divide("2", "1")
Ausführung der finally-Klausel
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 1, in <module>
divide("2", "1")
~~~~~~^^^^^^^^^^
Datei "<stdin>", Zeile 3, in divide
result = x / y
~~^~~
TypeError: Nicht unterstützte Operandentypen für /: 'str' und 'str'
Wie du sehen kannst, wird die finally Klausel in jedem Fall ausgeführt. Der TypeError, ausgelöst durch die Teilung zweier Zeichenfolgenn wird von der except Klausel nicht verarbeitet und daher nach Ausführung der finally Klausel erneut ausgelöst.
In realen Anwendungen ist die finally Klausel nützlich, um externe Ressourcen (z. B. Dateien oder Netzwerkverbindungen) freizugeben, unabhängig davon, ob die Nutzung der Ressource erfolgreich war.
8.8. Vordefinierte Bereinigungsaktionen¶
Einige Objekte definieren Standardbereinigungsaktionen, die durchgeführt werden, wenn das Objekt nicht mehr benötigt wird, unabhängig davon, ob der Vorgang mit dem Objekt erfolgreich war oder fehlgeschlagen ist. Schauen Sie sich das folgende Beispiel an, in dem versucht wird, eine Datei zu öffnen und ihren Inhalt auf dem Bildschirm auszugeben.
for line in open("myfile.txt"):
print(line, end="")
Das Problem bei diesem Code besteht darin, dass die Datei für eine unbestimmte Zeit geöffnet bleibt, nachdem die Ausführung dieses Teils des Codes abgeschlossen ist. Dies ist bei einfachen Skripten kein Problem, kann jedoch bei größeren Anwendungen ein Problem darstellen. Die with Anweisung ermöglicht die Verwendung von Objekten wie Dateien auf eine Weise, die sicherstellt, dass sie stets zeitnah und korrekt bereinigt werden.
with open("myfile.txt") as f:
for line in f:
print(line, end="")
Nach Ausführung der Anweisung wird die Datei f immer geschlossen, auch wenn bei der Verarbeitung der Zeilen ein Problem aufgetreten ist. Objekte, die wie Dateien vordefinierte Bereinigungsaktionen bereitstellen, weisen in ihrer Dokumentation darauf hin.
8.10. Anreicherung von Ausnahmen mit Notizen¶
Wenn eine Ausnahme erstellt wird, um ausgelöst zu werden, wird sie normalerweise mit Informationen initialisiert, die den aufgetretenen Fehler beschreiben. Es gibt Fälle, in denen es sinnvoll ist, Informationen hinzuzufügen, nachdem die Ausnahme abgefangen wurde. Zu diesem Zweck verfügen Ausnahmen über eine Methode add_note(note), welche einen String akzeptiert und zur Notizenliste der Ausnahme hinzufügt. Das Standard-Traceback-Rendering umfasst alle Notizen in der Reihenfolge, in der sie nach der Ausnahme hinzugefügt wurden.
>>> try:
... raise TypeError('falscher Typ')
... except Exception as e:
... e.add_note('Einige Informationen hinzufügen')
... e.add_note('Weitere Informationen hinzufügen')
... raise
...
Traceback (letzter Aufruf zuletzt):
Datei "<stdin>", Zeile 2, in <module>
raise TypeError('falscher Typ')
TypeError: falscher Typ
Füge einige Informationen hinzu
Füge noch mehr Informationen hinzu
>>>
Wenn wir beispielsweise Ausnahmen in einer Ausnahmegruppe sammeln, möchten wir möglicherweise Kontextinformationen für die einzelnen Fehler hinzufügen. Im Folgenden gibt es für jede Ausnahme in der Gruppe einen Hinweis, der angibt, wann dieser Fehler aufgetreten ist.
>>> def f():
... raise OSError('Vorgang fehlgeschlagen')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Aufgetreten in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('Es sind einige Probleme aufgetreten', excs)
+ Traceback der Ausnahmegruppe (letzter Aufruf zuletzt):
| Datei "<stdin>", Zeile 1, in <module>
| raise ExceptionGroup('Wir haben einige Probleme', excs)
| ExceptionGroup: Wir haben einige Probleme (3 Unterausnahmen)
+-+---------------- 1 ----------------
| Traceback (letzter Aufruf zuletzt):
| Datei "<stdin>", Zeile 3, in <module>
| f()
| ~^^
| Datei "<stdin>", Zeile 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Aufgetreten in Iteration 1
+---------------- 2 ----------------
| Traceback (letzter Aufruf zuletzt):
| Datei "<stdin>", Zeile 3, in <module>
| f()
| ~^^
| Datei "<stdin>", Zeile 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Aufgetreten in Iteration 2
+---------------- 3 ----------------
| Traceback (letzter Aufruf zuletzt):
| Datei "<stdin>", Zeile 3, in <module>
| f()
| ~^^
| Datei "<stdin>", Zeile 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Aufgetreten in Iteration 3
+------------------------------------
>>>