?>

Fehler und Ausnahmen

Ausnahmebehandlung

Python logo with band aid

Eine Ausnahme ist ein Fehler, der während der Ausführung eines Programms auftritt. Ausnahmen sind Nicht-Programmierern als Instanzen bekannt, die nicht mit einer allgemeinen Regel übereinstimmen. Der Name "Ausnahme" in der Informatik hat ebenfalls diese Bedeutung: Er impliziert, dass das Problem (die Ausnahme) nicht häufig auftritt, d. h. die Ausnahme ist die "Ausnahme von der Regel". Die Ausnahmebehandlung ist ein Konstrukt in einigen Programmiersprachen, um Fehler automatisch zu behandeln oder mit ihnen umzugehen. Viele Programmiersprachen wie C++, Objective-C, PHP, Java, Ruby, Python und viele andere haben eine eingebaute Unterstützung für die Ausnahmebehandlung.

Die Fehlerbehandlung wird im Allgemeinen dadurch gelöst, dass der Zustand der Ausführung zum Zeitpunkt des Auftretens des Fehlers gespeichert wird und der normale Programmfluss unterbrochen wird, um eine spezielle Funktion oder ein Stück Code auszuführen, das als Exception-Handler bezeichnet wird. Je nach Art des aufgetretenen Fehlers ("Division durch Null", "Datei-Öffnungsfehler" und so weiter) kann der Error-Handler das Problem "beheben" und das Programm kann anschließend mit den zuvor gespeicherten Daten fortgesetzt werden.

Ausnahmebehandlung in Python

Die Ausnahmebehandlung in Python ist der in Java sehr ähnlich. Der Code, der das Risiko einer Ausnahme birgt, wird in einen Try-Block eingebettet. Während in Java Ausnahmen durch catch-Klauseln abgefangen werden, haben wir in Python Anweisungen, die durch ein "except"-Schlüsselwort eingeleitet werden. Es ist möglich, "maßgeschneiderte" Ausnahmen zu erstellen: Mit der raise-Anweisung ist es möglich, das Auftreten einer bestimmten Ausnahme zu erzwingen.

Schauen wir uns ein einfaches Beispiel an. Angenommen, wir wollen den Benutzer auffordern, eine ganze Zahl einzugeben. Wenn wir ein input() verwenden, wird die Eingabe ein String sein, den wir in einen Integer umwandeln müssen. Wenn die Eingabe keine gültige Ganzzahl ist, wird ein ValueError erzeugt (raise). Wir zeigen dies in der folgenden interaktiven Sitzung:

n = int(input("Bitte gebe eine Zahl ein: "))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-3-b596fc0d106f> in <module>
----> 1 n = int(input("Bitte gebe eine Zahl ein: "))
ValueError: invalid literal for int() with base 10: 'vier'

Mit Hilfe der Ausnahmebehandlung können wir robusten Code für das Lesen eines Integers von der Eingabe schreiben:

while True:
    try:
        n = input("Bitte gebe eine ganze Zahl ein:  ")
        n = int(n)
        break
    except ValueError:
        print("Keine gültige Zahl, probiere es nochmals ...")
print("Großartig! Das war eine korrekte ganze Zahl")
Keine gültige Zahl, probiere es nochmals ...
Keine gültige Zahl, probiere es nochmals ...
Großartig! Das war eine korrekte ganze Zahl

Es handelt sich um eine Schleife, die nur dann abbricht, wenn eine gültige Ganzzahl angegeben wurde. Die while-Schleife wird eingegeben. Der Code innerhalb der try-Klausel wird Anweisung für Anweisung ausgeführt. Tritt während der Ausführung keine Exception auf, wird die Ausführung bis zur break-Anweisung geführt und die while-Schleife verlassen. Tritt eine Exception auf, z. B. beim Casting von n, wird der Rest des try-Blocks übersprungen und die except-Klausel ausgeführt. Der ausgelöste Fehler, in unserem Fall ein ValueError, muss mit einem der Namen hinter except übereinstimmen. In unserem Beispiel nur einer, nämlich "ValueError:". Nachdem der Text der print-Anweisung ausgegeben wurde, führt die Ausführung eine weitere Schleife aus. Sie beginnt mit einer neuen Input().

Mehrere Except-Klauseln

Eine try-Anweisung kann mehr als eine Except-Klausel für verschiedene Ausnahmen haben. Es wird aber höchstens eine Ausnahmeklausel ausgeführt.

Unser nächstes Beispiel zeigt eine try-Klausel, in der wir eine Datei zum Lesen öffnen, eine Zeile aus dieser Datei lesen und diese Zeile in eine Ganzzahl umwandeln. Es gibt mindestens zwei mögliche Ausnahmen:

Ein IOError
ValueError

Für den Fall, dass ein unerwarteter Fehler auftritt, haben wir zusätzlich eine unbenannte except-Klausel:

import sys
try:
    f = open('integers.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    errno, strerror = e.args
    print("I/O error({0}): {1}".format(errno,strerror))
    # e can be printed directly without using .args:
    # print(e)
except ValueError:
    print("Keine gültige ganze Zahl.")
except:
    print("Unerwarteter Fehler:", sys.exc_info()[0])
    raise
I/O error(2): No such file or directory

Die Behandlung des IOError im vorherigen Beispiel ist von besonderem Interesse. Die except-Klausel für den IOError spezifiziert eine Variable "e" hinter dem Ausnahmenamen (IOError). Die Variable "e" ist an eine Exception-Instanz mit den in instance.args gespeicherten Argumenten gebunden. Wenn wir das obige Skript mit einer nicht existierenden Datei aufrufen, erhalten wir die Meldung:

IOError(2): No such file or directory

Und wenn die Datei integers.txt nicht lesbar ist, z. B. wenn wir nicht die Berechtigung haben, sie zu lesen, erhalten wir die folgende Meldung:

I/O error(13): Permission denied

Eine except-Klausel kann mehr als eine Ausnahme in einem Tupel von Fehlernamen nennen, wie wir im folgenden Beispiel sehen:

try:
    f = open('integers.txt')
    s = f.readline()
    i = int(s.strip())
except (IOError, ValueError):
    print("Keine gültige ganze Zahl.")
except:
    print("Unerwarteter Fehler:", sys.exc_info()[0])
    raise
Keine gültige ganze Zahl.

Wir wollen nun demonstrieren, was passiert, wenn wir eine Funktion innerhalb eines Try-Blocks aufrufen und innerhalb des Funktionsaufrufs eine Exception auftritt:

def f():
    x = int("four")
try:
    f()
except ValueError as e:
    print("Hab' den Fehler :-) ", e)
print("Weiter geht's")
Hab' den Fehler :-)  invalid literal for int() with base 10: 'four'
Weiter geht's

die Funktion fängt die Ausnahme ab.

Wir werden unser Beispiel nun so erweitern, dass die Funktion die Exception direkt abfängt:

def f():
    try:
        x = int("four")
    except ValueError as e:
        print("Fehler in Funktion :-) ", e)
try:
    f()
except ValueError as e:
    print("got it :-) ", e)
print("Weiter geht's")
Fehler in Funktion :-)  invalid literal for int() with base 10: 'four'
Weiter geht's

Wie wir erwartet haben, wird die Ausnahme innerhalb der Funktion abgefangen und nicht in der Ausnahme des Aufrufers:

Wir fügen nun ein "raise" hinzu, das den ValueError erneut erzeugt, so dass die Exception an den Aufrufer propagiert wird:

def f():
    try:
        x = int("four")
    except ValueError as e:
        print("Fehler in Funktion :-) ", e)
        raise
        
try:
    f()
except ValueError as e:
    print("got it :-) ", e)
print("Weiter geht's")
Fehler in Funktion :-)  invalid literal for int() with base 10: 'four'
got it :-)  invalid literal for int() with base 10: 'four'
Weiter geht's

Benutzerdefinierte Ausnahmen

Es ist möglich, Ausnahmen selbst zu erstellen:

raise SyntaxError("Sorry, mein Fehler!")
Traceback (most recent call last):
  File "/home/bernd/anaconda3/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3319, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-13-0561d2cbb976>", line 1, in <module>
    raise SyntaxError("Sorry, mein Fehler!")
  File "<string>", line unknown
SyntaxError: Sorry, mein Fehler!

Der beste bzw. der pythonische Weg, dies zu tun, besteht darin, eine Ausnahmeklasse zu definieren, die von der Klasse Exception erbt. Sie müssen das Kapitel über Objektorientierte Programmierung durcharbeiten, um das folgende Beispiel vollständig zu verstehen:

class MyException(Exception):
    pass
raise MyException("Eine Ausnahme bestätigt nicht die Regel!")
---------------------------------------------------------------------------
MyException                               Traceback (most recent call last)
<ipython-input-14-e13d2e07cf7a> in <module>
      2     pass
      3 
----> 4 raise MyException("Eine Ausnahme bestätigt nicht die Regel!")
MyException: Eine Ausnahme bestätigt nicht die Regel!

Aufräum-Aktionen (try ... finally)

Bislang wurde die try-Anweisung immer mit except-Klauseln gepaart. Es gibt aber auch eine andere Möglichkeit, sie zu verwenden. Die try-Anweisung kann von einer finally-Klausel gefolgt werden. Finally-Klauseln werden als Aufräum- oder Beendigungsklauseln bezeichnet, weil sie unter allen Umständen ausgeführt werden müssen, d. h. eine "finally"-Klausel wird immer ausgeführt, unabhängig davon, ob in einem try-Block eine Ausnahme aufgetreten ist oder nicht. Ein einfaches Beispiel zur Demonstration der finally-Klausel:

try:
    x = float(input("Deine Zahl: "))
    inverse = 1.0 / x
finally:
    print("""Vielleicht ist eine Ausnahme 
    aufgetreten oder auch nicht.""")
print("Die inverse Zahl: ", inverse)
Your number: 34
There may or may not have been an exception.
The inverse:  0.029411764705882353

Kombination von try, except und finally

"finally" und "except" können zusammen für denselben try-Block verwendet werden, wie im folgenden Python-Beispiel zu sehen ist:

try:
    x = float(input("Deine Zahl: "))
    inverse = 1.0 / x
except ValueError:
    print("Das war wohl kein Zahl!")
except ZeroDivisionError:
    print("Unendlich")
finally:
    print("""Vielleicht ist eine Ausnahme 
    aufgetreten oder auch nicht.""")
print("Die inverse Zahl: ", inverse)
Vielleicht ist eine Ausnahme 
    aufgetreten oder auch nicht.
Die inverse Zahl:  0.022222222222222223

else-Klausel

Die try ... except-Anweisung hat eine optionale else-Klausel. Ein else-Block muss nach allen except-Klauseln platziert werden. Eine else-Klausel wird ausgeführt, wenn die try-Klausel keine Ausnahme auslöst.

Das folgende Beispiel öffnet eine Datei und liest alle Zeilen in eine Liste namens "text" ein:

file_name = "trullala.txt"
text = []
try:
    fh = open(file_name, 'r')
    text = fh.readlines()
    fh.close()
except IOError:
    print('Datei lässt sich nicht öffnen:', file_name)
if text:
    print(text[100])
Datei lässt sich nicht öffnen: trullala.txt

Im folgenden Beispiel haben wir die beiden Zeilen nach dem open-Aufruf unterhalb des else-Blockes geschrieben. Wenn es mit dem Öffnen klappt, wird die Ausführung mit dem else-Block fortgesetzt. Erfolgt jedoch eine Ausnahme, wird der else-Block übersprungen:

import sys
file_name = "fibonacci.py"
text = []
try:
    fh = open(file_name, 'r')
except IOError:
    print('cannot open', file_name)
else:
    text = fh.readlines()
    fh.close()
if text:
    print(text[10])
        return fib(n-1) + fib(n-2)

Die assert-Anweisung

Die assert-Anweisung ist für Debug-Aufgaben bestimmt: Sie kann als abgekürzte Schreibweise für eine bedingte raise-Anweisung angesehen werden, d.h. eine Ausnahme wird nur dann generiert, wenn eine bestimmte Bedingung nicht wahr ist.
Ohne die assert-Anweisung zu benutzen würden wir dies wie folgt in Python formulieren:

if not <some_test>:
        raise AssertionError(<message>)
Der folgende Code - unter Benutzung der assert-Anweisung - ist semantisch äquivalent, d.h. er hat die gleiche Bedeutung:

assert <some_test>, <message>
Die obige Zeile kann wie folgt "gelesen" werden: Falls <some_test> als False ausgewertet wird, wird eine Ausnahme generiert und <message> wird ausgegeben.

Beispiel:

x = 5
y = 3
assert x < y, "x has to be smaller than y"
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-28-246a6fdef85a> in <module>
      1 x = 5
      2 y = 3
----> 3 assert x < y, "x has to be smaller than y"
AssertionError: x has to be smaller than y

Hinweis: assert sollte nicht zum "Fangen" von Programmfehlern wie x / 0 benutzt werden, weil diese von Python selbst bestens erkannt und behandelt werden! assert sollte verwendet werden um bestimmte, vom Benutzer definierte, Einschränkungen zu "fangen".

In [ ]: