Link Search Menu Expand Document

Ausnahmen

Motivation

Man programmiert nicht nur für die sonnigen Tage oder anders ausgedrückt, in einem Programm kann viel schiefgehen. Deswegen ist es in jeder Programmiersprache wichtig, sich Gedanke zu machen, wie man Fehler signalisiert und behandelt.

Bei einem Programm können viele Dinge schiefgehen

  • Dateioperationen schlagen fehl
  • Werte haben den falschen Datentyp
  • Netzwerkverbindung wurde gekappt

Wie soll das Programm reagieren? Einfach abstürzen?

Die Programme, wie wir sie bisher geschrieben haben, stürzen bei einem Fehler einfach ab. Für einfache, kleine Berechnungen mag das ausreichen aber niemand möchte ein solches Programm als Benutzer einsetzen.

Schon eine falsche Benutzereingabe reicht für den Absturz, wenn man die Fehler nicht adäquat behandelt.

alter = float(input("Alter: "))
if alter >= 18:
    print("Du darfst Saw anschauen")
else:
    print("Leider nein, wie wäre es mit Frozen?")
Ausgabe
$ python3 kinokasse.py
Alter: Was geht dich das an?
Traceback (most recent call last):
  File "alter.py", line 1, in <module>
    alter = float(input("Alter: "))
ValueError: could not convert string to float: 'Was geht dich das an?'
$

try … except

Python benutzt für die Fehlerbehandlung ein Konzept, das man so auch bei anderen Programmiersprachen findet: Durch die Schlüsselworte try und except werden die möglicherweise fehlerbehafteten Anweisungen und die Behandlung der Fehler auf unterschiedliche Abschnitte des Quelltextes aufgeteilt.

Fehlerbehandlung in Python erfolgt über try und except

  • Die Befehle im try werden ausgeführt
  • Tritt kein Fehler auf
    • werden die Befehle im except ignoriert
    • läuft das Programm nach dem try/except weiter
  • Wenn ein Fehler auftritt
    • wird der Rest des try ignoriert
    • es wird nach einem except-Block gesucht, der zum Fehler passt
      → wird einer gefunden, wird er ausgeführt
      → wird keiner gefunden, bricht das Programm ab

Jeder Fehler (Exception) wird durch ein Objekt repräsentiert, dessen Klasse von Exception abgeleitet ist. Der Typ des Objektes liefert eine genaue Information, welche Art von Fehler aufgetreten ist.

Die Fehlerbehandlung wird von der normalen Programmlogik dadurch getrennt, dass man zwei unterschiedliche Blöcke verwendet. Der eine Block wird mit try eingeleitet und enthält die Anweisungen, die möglicherweise zu einem Fehler führen könnten, z. B. Zugriff auf eine Datei. Der andere Block wird mit except gekennzeichnet und enthält die Fehlerbehandlung. Er wird nur dann angesprungen, wenn im try-Block ein Fehler aufgetreten ist. Wenn ein solcher Fehler auftritt, wird der try-Block abrupt beendet, d. h. er wird nicht weiter ausgeführt, sondern die Programmausführung springt zu dem passenden except-Block. Man sagt auch, dass der except-Block die Ausnahme fängt (Fangen einer Ausnahme). Wenn kein Fehler auftritt, läuft der try-Block ganz normal zu Ende und der except-Block wird ignoriert. D. h. die Programmausführung geht nach dem letzen except-Block weiter.

Das folgende Beispiel zeigt die beiden Schlüsselworte in Aktion.

try:
    filename = sys.argv[1]
    k = int(sys.argv[2])
    print(handle(filename, k))
except IOError as ioe:
    print("File does not exist: {}".format(ioe))
except (ValueError, IndexError):
    print("Error in command line input")
    print("Run as: python wc.py <filename> <k>")
    print("where <k> is an integer")

Das Programm verarbeitet eine Datei, deren Name auf der Kommandozeile angegeben wurde. Weiterhin wird auf der Kommandozeile eine Ganzzahl angegeben, die bei der Verarbeitung der Datei verwendet wird.

Im Programm können nun verschiedene Fehler auftreten:

  1. IOError: Die Datei kann nicht geöffnet werden, weil sie z. B. nicht existiert.
  2. IndexError: Auf der Kommandozeilen könnten nicht die beiden benötigten Argumente angegeben worden sein.
  3. ValueError: Der zweite Parameter könnte sich nicht in eine Ganzzahl (int) konvertieren lassen.

Das Objekt der Ausnahme kann man durch den Zusatz as VARNAME in einer Variable speichern und dann während der Fehlerbehandlung auswerten.

Diese drei Fehler-Fälle werden im Beispiel in zwei except-Blöcken verarbeitet. Man hätte auch drei Blöcke nehmen können oder nur einen. Will man die Fälle 2 und 3 unterscheiden können, verwendet man drei Blöcke:

try:
    filename = sys.argv[1]
    k = int(sys.argv[2])
    print(handle(filename, k))
except IOError as ioe:
    print("File does not exist: {}".format(ioe))
except (ValueError, IndexError):
    print("Too few arguments")
    print("Run as: python wc.py <filename> <k>")
except ValueError:
    print("Error in command line input")
    print("No integer given")

try … except … else

Will man Operationen für den Fall durchführen, dass kein Fehler aufgetreten ist, dann kann man dies in einem else-Block durchführen.

Nach dem letzten except kann noch ein else-Block angegeben werden

  • wird ausgeführt, wenn keine Ausnahme aufgetreten ist
  • kann benutzt für Code werden, in dem Ausnahmen nicht gefangen werden sollen

Ein Beispiel für diesen Fall könnte z. B. das Speichern einer Datei sein.

try:
    f = open("data.txt", "w+")
    f.write("Data...")
except IOError:
    print("File does not exist")
else:
    f.close()
    print("File saved")

Warum schreibt man das close() nicht einfach in den try-Block? Dort würde es doch auch nicht ausgeführt, wenn ein Fehler auftritt, da der Block dann abrupt beendet wird.

Die Antwort liegt in der Frage, was passieren soll, wenn bei den Befehlen im else selbst wieder eine Ausnahme auftritt. Steht das close() im try dann würde bei einem Fehler während dieser Operation das except IOError: angesprungen. Schreibt man es aber in den else-Block, dann wird der Fehler nicht einfach behandelt, sondern sichtbar.

finally

Als Gegenstück zu else gibt es mit finally die Möglichkeit, einen Block anzugeben, der in jedem Fall ausgeführt wird, sowohl im Fehler als auch im Nicht-Fehlerfall.

Es kommt sehr häufig vor, dass man bestimmte Aktionen vorbereitet (z. B. eine Datei öffnet), dann eine Aktion durchführt (in die Datei schreibt) und dann Aufräumarbeiten durchführen muss (die Datei schließen). Wenn jetzt während der Durchführung ein Fehler auftreten kann, wird man entsprechende try-catch-Block benutzen. Tritt ein Fehler auf, so muss man in den meisten Fällen trotzdem die Aufräumarbeiten durchführen. Würde man diese im try-Block machen, würden sie im Fehlerfall nicht ausgeführt. Macht man sie im catch-Block, werden sie nur im Fehlerfall ausgeführt. Es fehlt also die Möglichkeit, Aufräumarbeiten sowohl im Fehler- als auch im Nicht-Fehler-Fall durchzuführen.

Genau zu diesem Zweck dient der finally-Block, der durch das Schlüsselwort finally eingeleitet wird. Die Anweisungen in diesem Block werden auf jeden Fall ausgeführt, egal, ob ein Fehler auftritt oder nicht. Sogar wenn der try- oder catch-Block mit return verlassen werden sollte, wird vorher noch der finally-Block ausgeführt, bevor die Methode zurückkehrt.

Zusätzlich kann ein finally-Block angegeben werden

  • wird immer ausgeführt
    • keine Exception geworfen
    • Exception geworfen aber gefangen
    • Exception geworfen aber nicht gefangen
  • wird benutzt, um Ressourcen zu schließen

Ein künstliches Beispiel, das die Funktionsweise des finally-Blocks verdeutlicht, folgt hier.

Beispiel: finally
def div(x, y):
    try:
        return x / y
    except ZeroDivisionError:
        print('Division by zero!')
    finally:
        print('Finally clause')

div(3, 2)
div(3, 0)
Ausgabe
Finally clause
1.5

Division by zero!
Finally clause

Ausnahme selbst werfen

Man kann nicht nur eigene Exceptions schreiben, man sollte es auch. Zu einem guten Programm gehören auch passende Ausnahmen. Allerdings sollte man dies nicht übertreiben und so häufig wie möglich auch vorhandene Exceptions von Python verwenden, da man hier dem Verwender des APIs bereits bekannte Ausnahmen anbieten kann.

Da Ausnahmen ganz normale Python-Objekte sind, muss man nur eine entsprechende Klasse schreiben, um eine eigene Art von Ausnahmen zu definieren. Die Ausnahme-Objekte werden, wie alle Objekte erzeugt.

Damit hat man aber erst ein Objekt erzeugt, es aber noch nicht auf den Weg gebracht. Hierzu muss man es mit dem Schlüsselwort raise werfen (Werfen von Ausnahmen).

  • Mit raise kann eine Ausnahme selbst ausgelöst (geworfen) werden
  • Man kann eigene Exception-Klassen schreiben
def div(x, y):
    if y == 0:
        raise ZeroDivisionError('So nicht mein Freund!')
    return x / y
REPL
>>> div(5, 2)
2.5

>>> div(2, 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in div
ZeroDivisionError: So nicht mein Freund!

Eine eigene Ausnahme muss von Exception abgeleitet werden. Die Konvention fordert, dass der Name auf Error endet.

class MeinError(Exception):
    pass

def magic(n):
    if n == 42:
        raise MeinError()

magic(1)
magic(42) # Error
# __main__.MeinError

Man kann die eigenen Ausnahmen auch mit weiteren Daten versehen, da sie normale Klassen sind.


Copyright © 2022 Thomas Smits