Zum Inhalt

Exception Handling

Mit Exception Handling, Außnahmebehandlung oder Fehlerbehandlung wird der Prozess bezeichnet, in dem fehlerhafte Programmzustände (hervorgerufen durch einen Bug) im Programm selbst bearbeitet werden. Durch die Weiterverarbeitung in einem anderen Teil des Programms kann ein Absturz des Programms möglicherweise verhindert werden.

Auslösen einer Exception

Innerhalb eines Python Programms kann durch das Schlüsselwort raise an jeder beliebigen Stelle im Quellcode eine Exception erzeugt werden. Eine Exception ist dabei immer eine Instanz einer Exception-Klasse. In Python gibt es eine Hierarchie von unterschiedlichen Exceptions, welche alle gängigen Fehlerklassen abbilden. Im folgenden Bild findet sich ein UML Klassendiagramm mit einem Auszug aus der Python Exception-Hierarchie:

Auszug Python Exception-Hierarchie

Generell sollten bestehende built-in Exception-Klassen verwendet werden um Fehlerzustände in eigenen Programmabschnitten zu markieren. Es können auch eigene Exception Klassen erstellt werden, es sollte jedoch genau geprüft werden, ob dies notwendig ist.

Im folgenden Beispiel wird die Funktion name_parts definiert. Diese Funktion soll einen Personennamen in seine Bestandteile aufteilen. Es wurde definiert, dass Namen die mehr als 4 Bestandteile aufweisen nicht unterstützt werden, dies wird durch das Werfen eines ValueError ausgedrückt.

def name_parts(name):
    name_parts = name.split()  # Überführt String in Liste mit Leerzeichen als Trenner (default)

    if len(name_parts) > 4:
        raise ValueError("names with more than 4 parts are not supported")

    return name_parts

    split_name("Alexander van der Bellen")  # ['Alexander', 'van', 'der', 'Bellen']
    split_name("Barnaby MarmadukeAloysius BenjyCobweb... Usansky")  # raise ValueError

Verwendung der Exception-Klasse ValueError

Im Beispiel wurde die Exception-Klasse ValueError verwendet. Laut Dokumentation soll die Klasse ValueError genau dann geworfen werden, wenn folgendes zutrifft:

Raised when an operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.

Verständnisfrage

Gehen Sie die built-in Exceptions der Python Dokumentation durch und überlegen Sie, welche Exception für die folgenden Fälle adequat sind:

  • Es wird versucht in einem Dictionary einen Schlüssel abzufragen, welcher nicht definiert wurde.
  • Eine Division durch 0.
  • Eine Datei zu öffnen, welche nicht existiert.

Die Fehlersuche in Programmen kann stark beschleunigt werden, wenn Sie ein Verständnis dafür aufbauen, welche Exceptions es gibt und wie diese generell ausgelöst werden. Mit etwas Erfahrung können Sie die Fehler im Quellcode auf Basis der Exception schnell finden und hoffentlich entsprechend ausbessern.

Abfangen von Exceptions

Falls eine Exception nicht explizit abgefangen wird, wird sie spätestens von der Laufzeitumgebung abgefangen. Dies würde beispielsweise durch die Ausgabe auf der Kommandozeile ersichtlich. Falls eine Exception nicht abgefangen wird, führt dies auch zum Absturz des Programms.

Durch ein gutes Exception Handling kann es möglich sein einen Programmabsturz zu verhindern. Im folgenden Quellcodeabschnitt wird gezeigt wie ein try/except Block in Python gestaltet werden kann. Die Exceptions werden dabei von oben nach unten abgearbeitet. Deshalb sollten sehr spezifische Exceptions weit oben stehen und je unspezifischer die Exceptions werden, desto weiter unten sollten diese stehen.

try:
    # Durchführung einer Berechnung    
    a = b / c

except ZeroDivisionError:
    # 1. Abwicklung Exception mit höchster Spezifität
    # Minimale Erhöhung der Wertes in c um den ZeroDivisionError zu verhindern
    c += 0.0000000000001
    a = b / c

except(TypeError, ValueError, ArithmeticError):
    # 2. Weitere Exceptionsmit niedriger Spezifität
    # Es können auch ExceptionGruppen zusammengefasst werden
    # ...
    pass

except:
    # Zuletzt können alle Exceptions behandelt werden,
    # welche nicht genauer spezifiziert sind
    # ...
    pass

Good Practice

Das letzte except Schlüsselwort stellt ein Catch-All dar. Das bedeuted, jegliche Exception würde durch diesen Block abgearbeitet. Die Software Engineering Community hat empfunden, dass generelle Exceptions nicht abgearbeitet werden sollten. Da eine Rettung des Programms bei einer unspezifizierten Exception eigentlich nicht möglich ist, sodass der Fehler eigentlich nur verschleppt wird. Siehe dazu eine Diskussion auf StackExchange.

Schlüsselwort finally

Mit dem Schlüsselwort finally wird ein Quellcodeabschnitt definiert, welcher ausgeführt wird egal ob ein Fehlerfall eingetreten ist oder nicht.

Im folgenden Beispiel wird eine Datei geöffnet und ausgelesen. Sollte es in der Verarbeitung der Datei zu einem Fehler kommen, wird die Datei in jedem Fall über den finally Quellcodeabschnitt geschlossen.

file = False

try:
    file = open("text.txt")
    i = 1    
    while True:
        line = file.readline()

        if len(line) == 0:
            break

        print("{}. line: ".format(i),line, end="")
        i +=1
except Exception as e:
    print("File Error: {}".format(e))

finally:
    if file:
        file.close()
        print("File closed")

Schlüsselwort assert

Mit einem Assert Statement können Annahmen über den Zustand von Variablen geprüft werden. Von den Annahmen wird ausgegangen, dass sie wahr sind und somit den Programmablauf korrekt ausführen würden. Wenn eine Annahme nicht zutrifft wird ein AssertionError geworfen. Assert Statements sind vorallem nützlich um Parameter einer Funktion auf Gültigkeit zu prüfen. In Python werden Assert Statements mit dem assert Schlüsselwort durchgeführt.

Syntaktisch wird ein Assert Statement in Python folgendermaßen niedergeschrieben:

assert condition, error message

Im folgenden Beispiel soll eine Funktion entwickelt werden, welche eine Vektoraddition durchführt. Die Vorbedingung für eine Vektoraddition ist, dass die beiden Vektoren dieselbe Länge aufweisen. Mittels eines Assert Statements kann diese Vorbedingung definiert werden:

def vector_addition(a, b):
    assert len(a) == len(b), "vectors must be of same size"
    return [i + j for i, j in zip(a, b)]

vector_addition([1, 2, 3], [4, 5, 6])  # [5, 7, 9]
vector_addition([1, 2, 3], [4, 5])     # AssertionError: vectors must be of same size  

with Statement

Das with Statement versucht komplexe Zusammenhänge als Clean Code darzustellen. Konkret würde dies bedeuten, dass der Try/Except Block wegfällt und Cleanup- bzw. Teardown-Logik implizit ausgeführt wird.

Mit der Implementierung der Magic Methods __enter__ bzw. __exit__ kann das with Statement mit einer Klasse genutzt werden. Im folgenden Quellcode Abschnitt wird dies exemplarisch dargestellt:

class WithExample:
    def __enter__(self):
        print("with enter")
        return "example result"

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("with exit")

with WithExample() as example:
    print("inside with: {}".format(example))

Der Quellcode würde in folgender Ausgabe resultieren:

with enter
inside with: example result
with exit

Ein gängiges Beispiel für das with Statement ist das Öffnen einer Datei. Da das gesamte File Handling (zB Schließen der Datei nach Verarbeitung) über die with Implementierung stattfinden würde. Im folgenden wird das Zeilenweise auslesen einer Datei über das with Statement demonstriert:

with open("demo.txt") as file:
    for line_index, line in enumerate(file):
        print("{}: {}".format(line_index + 1, line))

Ohne das with Statement würde das Öffnen und Auslesen einer Datei in etwa folgendermaßen aussehen:

try:
    file = open("demo.txt")
    for line_index, line in enumerate(file):
        print("{}: {}".format(line_index + 1, line))
except Exception as e:
    print("File Error: {}".format(e))
finally:
    if file:
        file.close()
        print("File closed")