2.26. Hibák kiváltása

Egy függvény úgy jelezhet problémát a hívójának, hogy kivált egy kivételt. A kulcsszó a raise:

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

A square_root(-1) hívása a raise sornál megáll, kiugrik a függvényből, és illeszkedő except blokkot keres a hívóban. Ha egyetlen hívó sem kapja el, a szkript hibakövetéssel (traceback) ér véget.

2.26.1. Miért kiváltani, ahelyett, hogy egy jelzőértéket adnánk vissza

Két módja a „rossz bemenet” jelzésének:

# 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

A kivételes forma általában jobb:

  • A hívónak szándékosan kell kezelnie a hibaesetet – vagy egy try blokkal, vagy úgy, hogy hagyja a kivételt továbbterjedni. A jelzőértékeket könnyű elfelejteni, és könnyű normál eredménynek nézni.

  • A hibaüzenet a kivétellel együtt utazik; a jelzőértékes megközelítésnek máshol kell csatolnia a diagnosztikai információt.

  • Az alapértelmezett viselkedés egy nem kezelt kivétel esetén egy hangos összeomlás, hibakövetéssel (traceback), amely a vétkes hívásra mutat. A néma None visszatérési értékek később finom hibákká válnak.

Csak akkor nyúlj jelzőértékekhez, amikor a „nem található” egy rutinszerű, nem kivételes kimenet – a dict.get() éppen azért ad vissza None értéket hiányzó kulcs esetén, mert a keresések várhatóan néha eredménytelenek.

2.26.2. Egyedi kivételosztályok

Ahhoz, hogy olyan problémát válts ki, amelyet a hívó esetleg meg akar különböztetni a beépített hibáktól, definiálj egy Exception alosztályt:

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)

Az üres class törzs rendben van – maga a név az, ami számít, mert a hívók osztály szerint kapnak el. Csoportosítsd a kapcsolódó hibákat egy közös alaposztály alá, ha egy hívó esetleg az egész családot egyetlen blokkban akarja elkapni.

2.26.2.1. Újrakiváltás

Egy üres raise egy except blokkon belül újra kiváltja az aktuális kivételt, így az a következő kezelőhöz terjed tovább:

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

Ez a helyes forma, amikor egy függvény meg akar figyelni egy hibát (naplózni, megszámolni, egy részleges változtatást visszavonni) anélkül, hogy ténylegesen kezelné.

2.26.3. Mikor kapd el, és mikor terjeszd tovább

Egy hasznos ökölszabály:

  • Kapd el a kivételt azon a szinten, amely értelmesen helyre tud állni – helyettesíts egy alapértelmezett értéket, próbáld újra, hagyd ki a rossz bemenetet.

  • Hagyd továbbterjedni, ha nincs semmi hasznos teendő az összeomláson kívül, vagy amikor a felette lévő réteg az, amely tudja, hogyan álljon helyre.

Egy hívási verem közepén lévő függvény, amely minden hibát elnyel, és némán visszatér, követhetetlenné teszi a hibákat. Inkább hagyd a kivételeket utazni, amíg el nem érnek olyan kódot, amelynek valóban van terve.