Python logo mit Pflaster

Fehlerbehandlung und Ausnahmebehandlung

Fehler werden häufig in anderen Programmiersprachen mit Fehlerrückgabewerten oder globalen Statusvariablen behandelt. Traditionelle Fehlerbehandlung bzw. Fehlervermeidung wird meistens in bedingten Anweisungen behandelt, so wie im folgenden Codefragment, in dem eine Division durch 0 verhindert werden soll:
if y != 0:
    z = x / y
Eleganter geht es mit den in Python vorhandenen Ausnahmebehandlungen.

Ausnahmebehandlung

Eine Ausnahme (exception) ist eine Ausnahmesituation (Fehler), die sich während der Ausführung eines Programmes einstellt. Unter einer Ausnahmebehandlung (exception handling) versteht man ein Verfahren, die Zustände, die während dieser Situation herrschen, an andere Programmebenen weiterzuleiten. Dadurch ist es möglich, per Programm einen Fehlerzustand gegebenenfalls zu "reparieren", um anschließend das Programm weiter auszuführen. Ansonsten würden solche Fehlerzustände in der Regel zu einem Abbruch des Programmes führen. Man verwendet den Begriff "Ausnahme" (oder englisch exception) um schon mit der sprachlichen Bezeichnung klar zu machen, dass es sich um einen außerordentlichen Zustand handelt, also die "Ausnahme von der Regel".

Viele Programmiersprachen so wie C++, Objective-C, PHP, Java, Ruby und Python besitzen integrierte Mechanismen mit eigenen formalen syntaktischen Strukturen, die sich von Sprache zu Sprache teils ähneln, teils erheblich unterscheiden, um Ausnahmebehandlungen zu ermöglichen.

Die Realisierung der Ausnahmebehandlung sieht meist so aus, dass automatisch, wenn eine Ausnahmesituation auftritt, Informationen und Zustände gespeichert werden, die zum Zeitpunkt der Ausnahme bzw. vor der Ausnahme geherrscht hatten.
An dieser Stelle wird der normale Programmfluss unterbrochen und ein spezielles Code-Fragment ausgeführt, der auch Exception-Handler genannt wird. Bedingt durch die Art des Fehlers ("Division durch 0", "Datei-Fehler", usw.) kann das spezielle Code-Fragment darauf reagieren. Danach kann wieder der normale Programmfluss aufgenommen werden mit den vorher gespeicherten Informationen und Zuständen.

Ausnahmebehandlung in Python

Die Ausnahmebehandlung in Python ist sehr ähnlich zu Java. Der Code, der das Risiko für eine Ausnahme beherbergt, wird in ein try-Block eingebettet. Aber während in Java Ausnahmen durch catch-Konstrukte abgefangen werden, geschieht dies in Python durch das except-Schlüsselwort. Semantisch funktioniert es aber genauso. Man kann auch Ausnahmen selbst erzeugen: Mit der raise-Anweisung ist es möglich eine bestimmte Ausnahme entstehen zu lassen.

Im folgenden Programm zeigen wir, wie wir die eingangs erwähnte Division durch 0 mittels try und except verhindern können:

x = 10
for y in [3, 0, 1]:
    try:
        z = x / y
    except:
        z = None
    print("z: ", z)
$ python3 ausnahme1.py 
z:  3.3333333333333335
z:  None
z:  10.0
Schauen wir uns ein einfaches Beispiel an. Ein Benutzer soll eine Integer-Zahl eingeben. Wenn wir nur ein input() benutzen, wird die Eingabe als String interpretiert, den wir dann in ein Integer wandeln müssen. Bei der Anwendung des cast-Operators, also bei der Wandlung des eingegebenen Strings in eine Integer-Zahl, kann es jedoch zu einem Fehler kommen, wenn der String kein gültiges Integer-Format aufzeigt. Es wird dann der Ausnahme-Fehler "ValueError" generiert. Wir zeigen dies in der folgenden kleinen interaktiven Sitzung:
>>> n = int(input("Please enter a number: "))
Please enter a number: 23.5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '23.5'


Mit Hilfe des Ausnahmebehandlung, können wir eine robuste Eingabeaufforderung zur Eingabe einer Integer-Zahl generieren:

while True:
    try:
        n = input("Bitte eine Ganzzahl (integer) eingeben: ")
        n = int(n)
        break
    except ValueError:
        print("Keine Integer! Bitte nochmals versuchen ...")
print("Super! Das war's!")
Es handelt sich um eine Schleife, die nur abbricht, wenn eine gültige Integer eingeben worden ist.
Das Beispiel-Skript funktioniert wie folgt:
Wenn die Schleife gestartet wird, werden die Anweisungen des try-Blocks nacheinander ausgeführt. Falls keine Ausnahme während der Ausführung auftritt, wird die break-Anweisung im try-Block erreicht und die while-Schleife wird abgebrochen. Wenn jedoch eine Ausnahme auftritt, d.h. beim Wandeln in integer mit int(), wird der Rest des try-Blockes übersprungen und der except-Block wird ausgeführt, aber nur, wenn der Fehlertyp - in unserem Fall ValueError - mit dem Ausnahmenamen nach dem Schlüsselwort except, also in unserem Beispiel "ValueError:" übereinstimmt. Dann werden alle Anweisungen im except-Block ausgeführt, in unserem Fall nur eine print-Anweisung. Danach wird die Schleife von Neuem durchlaufen.

Im folgenden sehen wir einen Aufruf unseres kleinen Skriptes mit fehlerhaften Eingaben:
$ python integer_read.py 
Bitte eine Ganzzahl (integer) eingeben: 42.0
Keine Integer! Bitte nochmals versuchen ...
Bitte eine Ganzzahl (integer) eingeben: abc
Keine Integer! Bitte nochmals versuchen ...
Bitte eine Ganzzahl (integer) eingeben: 42
Super! Das war's!
$

Mehrere Ausnahme-Blöcke

Zu einem try-Block können mehrere except-Blöcke gehören. Aber höchstens einer der Blöcke kann ausgeführt werden.

In unserem nächsten Beispiel zeigen wir einen try-Block, in dem wir eine Datei zum Lesen öffnen, eine Zeile aus dieser Datei lesen und diese Zeile dann in eine Ganzzahl wandeln. In unserem try-Block können prinzipiell zwei Ausnahmen auftreten:

Zur Sicherheit haben wir noch einen zusätzlichen except-Block ohne spezifischen Fehlertyp zum Abfangen eines unerwarteten Fehlers:
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("No valid integer in line.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise
Die Behandlung von IOError im vorigen Beispiel ist von besonderem Interesse. Die except-Klausel für IOError definiert eine Variable "e" nach IOError. Die Variable "e" ist an eine Ausnahmeinstanz gebunden deren Argumente in instance.args gespeichert sind.
Wenn wir das obige Skript mit einer nicht-existierenden Datei starten, erhalten wir folgende Meldung:
I/O error(2): No such file or directory
Falls die Datei integers.txt nicht lesbar ist, z.B. wenn wir nicht die Leseberechtigung haben, erhalten wir eine andere Meldung:
I/O error(13): Permission denied
Eine einzelne except-Anweisung kann auch gleichzeitig mehrere Fehler abfangen, die verschiedenen Fehlerarten werden dann in einem Tupel gelistet, wie wir im folgenden Beispiel sehen:
try:
    f = open('integers.txt')
    s = f.readline()
    i = int(s.strip())
except (IOError, ValueError):
    print("An I/O error or a ValueError occurred")
except:
    print("An unexpected error occurred")
    raise

Wir wollen nun zeigen, was passiert, wenn wir innerhalb eines try-Blocks eine Funktion aufrufen, in der dann eine Ausnahme generiert wird:
def f():
    x = int("four")

try:
    f()
except ValueError as e:
    print("got it :-) ", e)


print("Let's get on")
Aus dem Ergebnis erkennen wir, dass das except auch die Ausnahme aus der Funktion abfängt:
got it :-)  invalid literal for int() with base 10: 'four'
Let's get on
Wir erweitern unser Beispiel dahingehend, dass wir nun auch in der Funktion direkt die Ausnahme abfangen:
def f():
    try:
        x = int("four")
    except ValueError as e:
        print("got it in the function :-) ", e)

try:
    f()
except ValueError as e:
    print("got it :-) ", e)


print("Let's get on")
Nun wird erwartungsgemäß der Fehler in der Funktion abgefangen und nicht mehr im äußeren, also dem except nach dem Aufruf:
got it in the function :-)  invalid literal for int() with base 10: 'four'
Let's get on
Nun wollen wir den Fehler innerhalb der Funktion nicht nur auffangen sondern auch nach außen weiterreichen, d.h. wir fügen ein raise ein, dass den ValueError nochmals erhebt bzw. generiert:
def f():
    try:
        x = int("four")
    except ValueError as e:
        print("got it in the function :-) ", e)
        raise

try:
    f()
except ValueError as e:
    print("got it :-) ", e)

print("Let's get on")
Mit folgendem Ergebnis:
got it in the function :-)  invalid literal for int() with base 10: 'four'
got it :-)  invalid literal for int() with base 10: 'four'
Let's get on
Wir können die Ausnahme nun auch im aufrufenden Kontext auffangen.

Eigene Ausnahmen

Der beste Weg in Python ist, dass eine Ausnahme-Klasse definiert wird die von der Klasse Exception erbt. Um das folgende Beispiel vollständig zu verstehen, sollten Sie das Kapitel "Objektorientierte Programmierung" durcharbeiten.
class MyException(Exception):
	pass

raise MyException("Was falsch ist, ist falsch!")
Wenn Sie das Programm starten, erhalten Sie das folgende Ergebnis:
$ python3 exception_eigene_klasse.py 
Traceback (most recent call last): 
  File "exception_eigene_klasse.py", line 4, in <module> 
  raise MyException("Was falsch ist, ist falsch!") 
__main__.MyException: Was falsch ist, ist falsch! 

Finalisierungs-Aktionen bei der try-Anweisung

Bisher haben wir die try-Anweisungen immer nur im Zusammenspiel mit except-Klauseln benutzt. Aber es gibt noch eine andere Möglichkeit für try-Anweisungen. Die try-Anweisung kann von einer finally-Klausel gefolgt werden. Man bezeichnet sie auch als Finalisierungs- oder Terminierungsaktionen, weil sie immer unter allen Umständen ausgeführt werden müssen, und zwar unabhängig davon, ob eine Ausnahme im try-Block aufgetreten ist oder nicht.
Wir zeigen die Anwendung einer finally-Klausel in einem einfachen Beispiel:
try:
    x = float(input("Your number: "))
    inverse = 1.0 / x
finally:
    print("There may or may not have been an exception.")
print("The inverse: ", inverse)
Schauen wir uns die Ausgabe des vorigen Skriptes an. Zuerst geben wie eine korrekte Zahl ein, dann einen String, wodurch wir einen Fehler produzieren:
bernd@venus:~/tmp$ python finally.py 
Your number: 34
There may or may not have been an exception.
The inverse:  0.0294117647059
bernd@venus:~/tmp$ python finally.py 
Your number: Python
There may or may not have been an exception.
Traceback (most recent call last):
  File "finally.py", line 3, in <module>
    x = float(input("Your number: "))
ValueError: invalid literal for float(): Python
bernd@venus:~/tmp$ 

try, except und finally in einem Konstrukt

"finally" und "except" können zusammen in einem try-Block verwendet werden, wie wir im folgenden Beispielprogramm sehen können:
try:
    x = float(input("Your number: "))
    inverse = 1.0 / x
except ValueError:
    print("You should have given either an int or a float")
except ZeroDivisionError:
    print("Infinity")
finally:
    print("There may or may not have been an exception.")
Die Ausgabe des vorigen Skriptes, wenn es unter "finally2.py" abgespeichert wird, sieht wie folgt aus:
bernd@venus:~/tmp$ python finally2.py 
Your number: 37
There may or may not have been an exception.
bernd@venus:~/tmp$ python finally2.py 
Your number: seven 
You should have given either an int or a float
There may or may not have been an exception.
bernd@venus:~/tmp$ python finally2.py 
Your number: 0
Infinity
There may or may not have been an exception.
bernd@venus:~/tmp$ 

else-Block

Die try ... except Anweisung hat eine optionale else-Klausel. Ein else-Block muss immer hinter allen except-Anweisungen positioniert werden. Ein else-Block wird ausgeführt, falls keine Ausnahme im try-Block auftritt.

Im folgenden Beispiel wird eine Datei zum Lesen geöffnet und alle Zeilen werden in eine Liste namens "text" eingelesen:
import sys
file_name = sys.argv[1]
text = []
try:
    fh = open(file_name, 'r')
    text = fh.readlines()
    fh.close()
except IOError:
    print('cannot open', file_name)

if text:
    print(text[100])
Semantisch ist das vorige Skript nahezu identisch mit dem folgenden:
import sys
file_name = sys.argv[1]
text = []
try:
    fh = open(file_name, 'r')
except IOError:
    print('cannot open', file_name)
else:
    text = fh.readlines()
    fh.close()

if text:
    print(text[100])
Der wesentliche Unterschied besteht darin, dass im ersten Fall, alle Anweisungen des try-Blocks zur gleichen Fehlermeldung "cannot open ..." führen, falls in ihnen eine Fehler auftritt. Diese Fehlermeldung ist für fh.close() und fh.readlines() irreführend.

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"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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".