Python logo mit Pflaster

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 solceh 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 teil 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. In den meisten Sprachen, so in C++, Java, PHP und auch in Python, werden Codeteile, die mit der Ausnahmebehandlung ausgeführt werden sollen, in einem try-Block zusammengefasst.

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 eine mögliche eine bestimmte Ausnahme entstehen zu lassen.

Schauen wir uns ein einfaches Beispiel an. Ein Benutzer soll eine Integer-Zahl eingeben. Wenn wir nur ein raw_input() benutzen, wird die Eingabe als String interpretiert, den wir dann in ein Integer wandeln müssen. Bei der Anwendung des cast-Operators 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(raw_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 = raw_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 mit einem ValueError 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 (errno, strerror):
    print "I/O error({0}): {1}".format(errno, strerror)
except ValueError:
    print "No valid integer in line."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise
The handling of the IOError in the previous example is of special interest. The IOError returns a tupel with an error number and a string with the error message, which we assign to the variables errno and strerror by "except IOError as (errno, strerror)".
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

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(raw_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 rine 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(raw_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(raw_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".s