?>

Fehler und Ausnahmen

Ausnahmebehandlung

Python logo with band aid

Python unterscheidet unter anderem Syntaxfehler und Ausnahmen (Exceptions), die während der Ausführung auftreten. Eine Ausnahme ist zunächst ein Signal dafür, dass der normale Programmfluss nicht fortgesetzt werden kann. Viele Ausnahmen repräsentieren Fehlerzustände – beispielsweise ungültige Eingaben oder fehlende Dateien –, einige dienen aber auch speziellen Steuerungszwecken.

Mit try und except kann ein Programm erwartbare Ausnahmesituationen gezielt behandeln. Dabei sollte man möglichst spezifische Exception-Klassen abfangen. Ein nacktes except: fängt auch Systemausnahmen wie KeyboardInterrupt und SystemExit; in normalem Anwendungscode ist except Exception as exc: meist die bessere allgemeine Grenze.

Eine Ausnahme, für die kein passender Handler gefunden wird, propagiert durch die Aufrufkette weiter und beendet schließlich den aktuellen Programmablauf mit einem Traceback.

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 mehrere Handler für unterschiedliche Exception-Typen enthalten. Höchstens ein passender Handler wird ausgeführt.

Beim Öffnen und Auswerten einer Textdatei sind beispielsweise OSError (Datei-/Betriebssystemfehler) und ValueError (ungültige Zahl) naheliegende Fälle. IOError ist in modernem Python lediglich ein Alias von OSError.

try:
    with open("integers.txt", encoding="utf-8") as f:
        s = f.readline()
    i = int(s.strip())
except OSError as exc:
    print(f"I/O-Fehler: {exc}")
except ValueError:
    print("Keine gültige ganze Zahl.")
except Exception as exc:
    print("Unerwarteter Fehler:", type(exc).__name__, exc)
    raise

Die Variable exc ist an die konkrete Exception-Instanz gebunden. OSError besitzt bei Betriebssystemfehlern zusätzliche Attribute wie errno, strerror und gegebenenfalls filename. Häufig reicht es jedoch, die Exception direkt auszugeben.

Mehrere Exception-Typen können in einer except-Klausel als Tupel angegeben werden:

try:
    with open("integers.txt", encoding="utf-8") as f:
        i = int(f.readline().strip())
except (OSError, ValueError) as exc:
    print("Datei konnte nicht passend verarbeitet werden:", exc)

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

Hier wird die Ausnahme nicht in f(), sondern im aufrufenden try-Block abgefangen. Im nächsten Beispiel behandelt dagegen die Funktion selbst den ValueError:

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

Im nächsten Schritt behandeln wir die Exception zunächst in der Funktion und lösen sie anschließend mit einem nackten raise erneut aus. Dadurch bleibt der ursprüngliche Traceback erhalten und die Ausnahme kann vom Aufrufer weiterbehandelt werden:

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

Eigene Exception-Typen sind sinnvoll, wenn ein Anwendungsbereich einen eigenen Fehlerzustand ausdrücken soll. Benutzerdefinierte Ausnahmen sollten normalerweise von Exception erben, nicht direkt von BaseException. Ein sprechender Name mit dem Suffix Error erleichtert die Verwendung:

class UngueltigesAlterError(Exception):
    """Wird ausgelöst, wenn eine Altersangabe außerhalb des erlaubten Bereichs liegt."""


def pruefe_alter(alter):
    if not 0 <= alter <= 130:
        raise UngueltigesAlterError(f"Unplausibles Alter: {alter}")

pruefe_alter(150)

Der neue Exception-Typ kann anschließend genauso gezielt mit except UngueltigesAlterError: behandelt werden wie eine eingebaute Ausnahme.

try:
    pruefe_alter(150)
except UngueltigesAlterError as exc:
    print(exc)

Aufräum-Aktionen mit finally

Eine finally-Klausel wird beim Verlassen des zugehörigen try-Blocks ausgeführt – unabhängig davon, ob der Block normal endet, eine Ausnahme behandelt wird oder eine Ausnahme weiterpropagiert. finally behandelt eine Ausnahme jedoch nicht automatisch; ohne passenden except-Handler wird sie nach dem finally-Block weitergereicht.

Für Ressourcen wie Dateien ist häufig ein Context Manager (with) klarer als manuelles Aufräumen in finally.

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, else und finally

  • except behandelt passende Ausnahmen aus dem try-Block.
  • else läuft nur, wenn im try-Block keine Ausnahme aufgetreten ist.
  • finally läuft beim Verlassen des Konstrukts in jedem Fall.

Das else ist besonders nützlich, um Code, der selbst neue Ausnahmen auslösen kann, nicht unnötig in den geschützten try-Block aufzunehmen:

try:
    x = float(input("Deine Zahl: "))
    inverse = 1.0 / x
except ValueError:
    print("Das war keine Zahl!")
except ZeroDivisionError:
    print("Durch Null kann nicht dividiert werden.")
else:
    print("Die inverse Zahl:", inverse)
finally:
    print("Berechnung beendet.")

else-Klausel

Ein else-Block steht nach allen except-Klauseln und wird ausgeführt, wenn der try-Block ohne Ausnahme beendet wurde. Bei Dateioperationen lässt sich damit beispielsweise das Öffnen von der anschließenden Verarbeitung trennen:

file_name = "trullala.txt"
try:
    fh = open(file_name, encoding="utf-8")
except OSError as exc:
    print("Datei lässt sich nicht öffnen:", exc)
else:
    with fh:
        text = fh.readlines()
    print(f"{len(text)} Zeile(n) gelesen")

Noch kompakter ist es häufig, den Context Manager direkt innerhalb von try zu verwenden, wenn die gesamte Dateioperation als eine Einheit behandelt werden soll:

file_name = "fibonacci.py"
try:
    with open(file_name, encoding="utf-8") as fh:
        text = fh.readlines()
except OSError as exc:
    print("Datei lässt sich nicht öffnen:", exc)
else:
    print(f"{len(text)} Zeile(n) gelesen")

Die assert-Anweisung

assert ist für interne Annahmen und Debugging-Invarianten gedacht. Formal entspricht

assert bedingung, nachricht

bei aktivem Debug-Modus ungefähr:

if not bedingung:
    raise AssertionError(nachricht)

Assertions dürfen nicht für die Validierung externer Eingaben, Berechtigungsprüfungen oder andere zwingend notwendige Laufzeitkontrollen verwendet werden. Python kann Assertions bei optimierter Ausführung (python -O) vollständig entfernen.

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

assert ist also sinnvoll, um Annahmen des Programmierers über den internen Zustand zu dokumentieren und während Entwicklung und Tests zu prüfen. Erwartbare Benutzer- oder Dateifehler behandelt man dagegen mit normaler Validierung und geeigneten Exceptions.