2.26. Sollevare errori#

Una funzione può segnalare un problema al suo chiamante sollevando un’eccezione. La parola chiave è raise:

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

Chiamare square_root(-1) si interrompe alla riga raise, esce dalla funzione e cerca un except corrispondente nel chiamante. Se nessun chiamante la intercetta, lo script termina con un traceback.

2.26.1. Perché sollevare un’eccezione invece di restituire un valore sentinella#

Due modi per segnalare un «input errato»:

# 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

La forma con eccezione è di solito migliore:

  • Il chiamante deve gestire deliberatamente il caso di errore – o con un try, oppure lasciando che l’eccezione si propaghi. I valori sentinella sono facili da dimenticare e facili da scambiare per un risultato normale.

  • Il messaggio di errore viaggia insieme all’eccezione; l’approccio con sentinella deve allegare la diagnostica da qualche altra parte.

  • Il comportamento predefinito in caso di eccezione non gestita è un crash evidente con un traceback che punta alla chiamata incriminata. I ritorni silenziosi di None diventano in seguito bug subdoli.

Ricorri ai valori sentinella solo quando «non trovato» è un esito di routine, non eccezionale – dict.get() restituisce None su una chiave mancante proprio perché ci si aspetta che le ricerche a volte falliscano.

2.26.2. Classi di eccezione personalizzate#

Per sollevare un problema che il chiamante potrebbe voler distinguere dagli errori built-in, definisci una sottoclasse di 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)

Il corpo vuoto della class va bene – ciò che conta è il nome stesso, perché i chiamanti intercettano per classe. Raggruppa gli errori correlati sotto una base comune se un chiamante potrebbe voler intercettare l’intera famiglia in un solo blocco.

2.26.2.1. Risollevare#

Un raise nudo all’interno di un blocco except risolleva l’eccezione corrente in modo che si propaghi al gestore successivo:

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

Questa è la forma corretta quando una funzione vuole osservare un errore (registrarlo, contarlo, annullare una modifica parziale) senza effettivamente gestirlo.

2.26.3. Quando intercettare e quando propagare#

Una regola pratica utile:

  • Intercetta un’eccezione al livello che può recuperare in modo significativo – sostituire un valore predefinito, riprovare, saltare l’input errato.

  • Lasciala propagare quando non c’è nulla di utile da fare se non andare in crash, oppure quando il livello superiore è quello che sa come recuperare.

Una funzione nel mezzo di uno stack di chiamate che inghiotte ogni errore e ritorna in silenzio rende i fallimenti non tracciabili. Preferisci lasciare che le eccezioni viaggino finché non raggiungono il codice che ha davvero un piano.