2.26. Fehler auslösen

Eine Funktion kann ihrem Aufrufer ein Problem signalisieren, indem sie eine Ausnahme auslöst (raise). Das Schlüsselwort lautet raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

Der Aufruf von square_root(-1) stoppt bei der raise-Zeile, springt aus der Funktion heraus und sucht im Aufrufer nach einem passenden except. Wenn kein Aufrufer es abfängt, endet das Skript mit einem Traceback.

2.26.1. Warum auslösen statt einen Sentinel zurückzugeben

Zwei Möglichkeiten, „ungültige Eingabe“ zu melden:

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

Die Ausnahme-Variante ist in der Regel besser:

  • Der Aufrufer muss den Fehlerfall bewusst behandeln – entweder mit einem try oder indem er die Ausnahme propagieren lässt. Sentinels werden leicht vergessen und leicht mit einem normalen Ergebnis verwechselt.

  • Die Fehlermeldung reist mit der Ausnahme mit; beim Sentinel-Ansatz muss die Diagnose irgendwo anders angehängt werden.

  • Das Standardverhalten bei einer nicht behandelten Ausnahme ist ein lauter Absturz mit einem Traceback, der auf den verursachenden Aufruf zeigt. Stille None-Rückgaben werden später zu subtilen Fehlern.

Greifen Sie nur dann zu Sentinels, wenn „nicht gefunden“ ein routinemäßiges, nicht außergewöhnliches Ergebnis ist – dict.get() gibt bei einem fehlenden Schlüssel genau deshalb None zurück, weil erwartet wird, dass Nachschlagevorgänge manchmal ins Leere greifen.

2.26.2. Benutzerdefinierte Ausnahmeklassen

Um ein Problem auszulösen, das der Aufrufer möglicherweise von eingebauten Fehlern unterscheiden möchte, definieren Sie eine Unterklasse von Exception:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

Der leere class-Körper ist in Ordnung – der Name selbst ist das Entscheidende, denn Aufrufer fangen nach Klasse ab. Gruppieren Sie verwandte Fehler unter einer gemeinsamen Basis, falls ein Aufrufer die ganze Familie in einem Block abfangen möchte.

2.26.2.1. Erneutes Auslösen

Ein bloßes raise innerhalb eines except-Blocks löst die aktuelle Ausnahme erneut aus, sodass sie zum nächsten Handler propagiert:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

Dies ist die richtige Form, wenn eine Funktion einen Fehler beobachten möchte (ihn protokollieren, zählen, eine teilweise Änderung rückgängig machen), ohne ihn tatsächlich zu behandeln.

2.26.3. Wann abfangen und wann propagieren

Eine nützliche Faustregel:

  • Fangen Sie eine Ausnahme auf der Ebene ab, die sich sinnvoll erholen kann – einen Standardwert einsetzen, erneut versuchen, die fehlerhafte Eingabe überspringen.

  • Lassen Sie sie propagieren, wenn es nichts Nützliches zu tun gibt außer abzustürzen, oder wenn die darüberliegende Schicht diejenige ist, die weiß, wie man sich erholt.

Eine Funktion mitten in einem Aufrufstapel, die jeden Fehler verschluckt und stillschweigend zurückkehrt, macht Fehlschläge unauffindbar. Lassen Sie Ausnahmen lieber so lange reisen, bis sie Code erreichen, der wirklich einen Plan hat.