2.26. Kasta fel

En funktion kan signalera ett problem till sin anropare genom att kasta ett undantag. Nyckelordet är raise:

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

Att anropa square_root(-1) stoppar vid raise-raden, hoppar ut ur funktionen och letar efter ett matchande except i anroparen. Om ingen anropare fångar det avslutas skriptet med en stackspårning.

2.26.1. Varför kasta istället för att returnera ett sentinelvärde

Två sätt att rapportera ”dålig indata”:

# 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

Undantagsformen är vanligtvis bättre:

  • Anroparen måste medvetet hantera felfallet – antingen med ett try, eller genom att låta undantaget fortplantas. Sentinelvärden är lätta att glömma och lätta att förväxla med ett normalt resultat.

  • Felmeddelandet följer med undantaget; sentinelmetoden måste fästa diagnostiken någon annanstans.

  • Standardbeteendet vid ett ohanterat undantag är en högljudd krasch med en stackspårning som pekar på det felaktiga anropet. Tysta None-returer blir subtila buggar senare.

Använd sentinelvärden endast när ”hittades inte” är ett rutinmässigt, icke-exceptionellt utfall – dict.get() returnerar None vid en saknad nyckel just för att uppslagningar förväntas ibland missa.

2.26.2. Anpassade undantagsklasser

För att kasta ett problem som anroparen kanske vill skilja från inbyggda fel, definiera en underklass till 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)

Den tomma class-kroppen är bra – namnet i sig är det som spelar roll, eftersom anropare fångar efter klass. Gruppera relaterade fel under en gemensam bas om en anropare kan vilja fånga hela familjen i ett block.

2.26.2.1. Återkastning

Ett naket raise inuti ett except-block återkastar det aktuella undantaget så att det fortplantas till nästa hanterare:

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

Detta är rätt form när en funktion vill observera ett fel (logga det, räkna det, ångra en partiell ändring) utan att faktiskt hantera det.

2.26.3. När man ska fånga och när man ska låta fortplantas

En användbar tumregel:

  • Fånga ett undantag på den nivå som kan meningsfullt återhämta sig – ersätt med ett standardvärde, försök igen, hoppa över den dåliga indatan.

  • Låt det fortplantas när det inte finns något användbart att göra utom att krascha, eller när lagret ovanför är det som vet hur man återhämtar sig.

En funktion mitt i en anropsstack som sväljer varje fel och returnerar tyst gör misslyckanden ospårbara. Föredra att låta undantag färdas tills de når kod som verkligen har en plan.