Exceptions#

Fehler mit Exceptions behandeln#

Exceptions unterbrechen den normalen Verlauf eines Programmes in einem Fehlerfall. Sie dienen dazu den Fehler im Programm zu kommunizieren (sie haben meist eine Fehlermeldung) und werden genutzt, um im Fehlerfall unkontrolliertes Abstürzen des Programmes zu verhindern.

Bekannte Exceptions sollten immer abgefangen. In Python werden sie durch den try-except-Block abgefangen. Dieser startet mit einem try und endet mit einem except. Nach dem try folgt der Block mit der bekannten (oder unbekannten) Exception. Nach dem except folgt der Block zur Fehlerbehandlung.

Es ist zu beachten, dass bei Exceptions keine Wertzuweisung stattfindet (der Wert ist ja nicht bekannt). Betrachten wir mal wieder, die aus der Verzweigung bekannte Division durch 0 mit ihren verschiedenen Fehlerfällen der Null-Division und falscher Datentypen der Eingaben. Ohne diese Fehlerfälle genau zu spezifizieren können wir schreiben.

zaehler = 10
nenner = 0
ergebnis = None
try:
    # Block mit bekannter Exception
    ergebnis = zaehler / nenner
    # Dieser Teil wird nur ausgeführt wenn keine Exception auftrat
    print("Die Division war erfolgreich")
except:
    # Fehlerbehandlung
    print("Divisionsfehler")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Divisionsfehler
Das Ergebnis von 10/0 = None

Das except is in dem obigen Fall untypisiert. Damit lassen sich alle Fehler, die auftreten können abfangen (auch unbekannte). Damit fängt der try-except-Block auch Fehler eines falschen Datentyps ab.

zaehler = 'keine_zahl'
nenner = 2
ergebnis = None
try:
    ergebnis = zaehler / nenner
except:
    print("Divisionsfehler")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Divisionsfehler
Das Ergebnis von keine_zahl/2 = None

Das Problem hierbei ist, dass wir keine Information über die Exception erhalten. Deshalb ist es immer sinnvoll die genaue Exception abzufangen und die Fehlermeldung auszugeben. Der allgemeinste Fehlertyp in Python ist die Exception welche wir wie folgt als Variable e abfangen.

zaehler = 'keine_zahl'
nenner = 0
ergebnis = None
try:
    ergebnis = zaehler / nenner
except Exception as e:
    print(f"Divisionsfehler mit Fehler vom typ `{type(e)}` und Meldung `{e}`")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Divisionsfehler mit Fehler vom typ `<class 'TypeError'>` und Meldung `unsupported operand type(s) for /: 'str' and 'int'`
Das Ergebnis von keine_zahl/0 = None

Sind die Exceptions bekannt sollten sie auch typisiert behandelt werden, um spezifische Meldungen auszugeben.

zaehler = 'keine_zahl'
nenner = 0
ergebnis = None
try:
    ergebnis = zaehler / nenner
except TypeError as e:
    print(f"Zaehler oder Nenner nicht vom Datentyp `int`")
except ZeroDivisionError as e:
    print("Teilung durch 0")

print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Zaehler oder Nenner nicht vom Datentyp `int`
Das Ergebnis von keine_zahl/0 = None

Der try-except-Block unterstütz auch die else-Anweisung, welche immer ausgeführt wird, wenn keine Exception auftritt. Ferner gibt es die finally-Anweisung, welche immer ausgeführt wird also im fehlerfreien Fall und im Fehlerfall.

Soll zum Beispiel eine Ausgabe nur gemacht werden, wenn der try-Block erfolgreich war, nutzen wir else.

zaehler = 2
nenner = 1
ergebnis = None
try:
    ergebnis = zaehler / nenner
except TypeError as e:
    print(f"Zaehler oder Nenner nicht vom Datentyp `int`")
except ZeroDivisionError as e:
    print("Teilung durch 0")
else:
    print(f"Die Division war erfolgreich und resultiert in {zaehler}/{nenner} = {ergebnis}")
Die Division war erfolgreich und resultiert in 2/1 = 2.0

Soll immer eine Ausgabe erfolgen können wir finally verwenden. Dies wird häufig verwendet, um Schritte durchzuführen die auch im Fehlerfall gemacht werden sollen, z.B. um Verbindungen zu Dateien oder zu einer Datenbank zu schließen, damit diese nicht unendlich lange offen bleiben (und irgendwann zu Fehlern führen).

zaehler = 'keine_zahl'
nenner = 0
ergebnis = None
try:
    ergebnis = zaehler / nenner
except TypeError as e:
    print(f"Zaehler oder Nenner nicht vom Datentyp `int`")
except ZeroDivisionError as e:
    print("Teilung durch 0")
finally:
    print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
Zaehler oder Nenner nicht vom Datentyp `int`
Das Ergebnis von keine_zahl/0 = None

Exceptions selbst erzeugen#

Exceptions können auch selbst mit raise erzeugt werden. Das ist sinnvoll, wenn in dem eigenen Code Fehler auftreten können, die woanders behandelt werden müssen. Hierbei muss ein Fehlertyp angegeben werden. Entweder man nutzt den allgemeinsten Fehlertyp Exception, einen passenden Standardfehler von Python oder einen selbst definierten. Es ist ratsam meist spezifische Fehlertypen zu nutzen, da der Typ des Fehlers viel beim Behandeln und Debuggen hilft.

Definieren wir als Beispiel unsere eigene Divisionsfunktion mit klarer Fehlerbenenung ob Zähler oder Nenner vom falschen Typ sind. Hier wollen wir, dass anstatt des TypeError ein ValueError erzeugt wird. Bei einer Division durch 0 soll anstatt eines Fehlers der Wert None zurückgegeben werden.

def division(zaehler, nenner):
    if not isinstance(nenner, (int, float)):
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
    elif not isinstance(zaehler, (int, float)):
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
    elif nenner == 0:
        print("Warnung Division durch 0")
        return None
    else:
        ergebnis = zaehler / nenner
        print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
        return ergebnis
zaehler = 'keine_zahl'
nenner = 0

ergebnis = division(zaehler, nenner)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 4
      1 zaehler = 'keine_zahl'
      2 nenner = 0
----> 4 ergebnis = division(zaehler, nenner)

Cell In[7], line 5, in division(zaehler, nenner)
      3     raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
      4 elif not isinstance(zaehler, (int, float)):
----> 5     raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
      6 elif nenner == 0:
      7     print("Warnung Division durch 0")

ValueError: Zaehler nicht vom Datentyp `int` oder `float`
zaehler= 10
nenner = 2
zaehler = 10
nenner = 1

ergebnis = division(zaehler, nenner)
Das Ergebnis von 10/1 = 10.0

Fehlerbehandlung bei Funktionen im Stack#

Exceptions werden im Stack (Aufrufreihenfolge der Funktionen) nach oben weitergegeben, bis sie entweder abgefangen und behandelt werden oder das Programm abstürzt, wenn das Ende des Stacks erreicht wird. Ziel des Abfangens und Behandelns von Exceptions ist es das Programm zurück in einen Zustand zu bringen in dem es weiterlaufen kann.

Als Beispiel wollen wir eine rekursive Funktion definieren, welche uns einen Fehler ausgibt, wenn die Rekurssionstiefe zu groß wird um einen Stack-Overflow-Fehler zu vermeiden. Wir nutzen als Beispiel die Fakultät aus dem Rekursionbeispiel.

def factorial_recursiv(x, depth=1):
    if depth > 20:
        raise RecursionError("Recursion zu tief")
    if x > 1:
        return x * factorial_recursiv(x-1, depth+1)
    else:
        return 1
factorial_recursiv(20)
2432902008176640000
factorial_recursiv(21)
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
Cell In[13], line 1
----> 1 factorial_recursiv(21)

Cell In[11], line 5, in factorial_recursiv(x, depth)
      3     raise RecursionError("Recursion zu tief")
      4 if x > 1:
----> 5     return x * factorial_recursiv(x-1, depth+1)
      6 else:
      7     return 1

Cell In[11], line 5, in factorial_recursiv(x, depth)
      3     raise RecursionError("Recursion zu tief")
      4 if x > 1:
----> 5     return x * factorial_recursiv(x-1, depth+1)
      6 else:
      7     return 1

    [... skipping similar frames: factorial_recursiv at line 5 (17 times)]

Cell In[11], line 5, in factorial_recursiv(x, depth)
      3     raise RecursionError("Recursion zu tief")
      4 if x > 1:
----> 5     return x * factorial_recursiv(x-1, depth+1)
      6 else:
      7     return 1

Cell In[11], line 3, in factorial_recursiv(x, depth)
      1 def factorial_recursiv(x, depth=1):
      2     if depth > 20:
----> 3         raise RecursionError("Recursion zu tief")
      4     if x > 1:
      5         return x * factorial_recursiv(x-1, depth+1)

RecursionError: Recursion zu tief

In der obigen Fehlermeldung bringt das Programm selbst zum erliegen (es stürtzt allerdings nicht der ganze Kernel ab, wie beim Stack-Overflow-Fehler). Um das zu vermeiden müssen wir den Fehler mit try-except abfangen. In der Fehlermeldung oben deutet sich schon der Stack-Trace an, also die Liste der Funktionsaufrufe die auf dem Stack gesammelt wurde.

import traceback
try:
    factorial_recursiv(21)
except RecursionError as e:
    print(e)
    traceback.print_exc()
Recursion zu tief
Traceback (most recent call last):
  File "/tmp/ipykernel_2408/841887965.py", line 3, in <module>
    factorial_recursiv(21)
  File "/tmp/ipykernel_2408/43912226.py", line 5, in factorial_recursiv
    return x * factorial_recursiv(x-1, depth+1)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/ipykernel_2408/43912226.py", line 5, in factorial_recursiv
    return x * factorial_recursiv(x-1, depth+1)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/ipykernel_2408/43912226.py", line 5, in factorial_recursiv
    return x * factorial_recursiv(x-1, depth+1)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 17 more times]
  File "/tmp/ipykernel_2408/43912226.py", line 3, in factorial_recursiv
    raise RecursionError("Recursion zu tief")
RecursionError: Recursion zu tief