2.26. Generarea erorilor

O funcție poate semnala o problemă apelantului ei generând (raising) o excepție. Cuvântul-cheie este raise:

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

Apelarea square_root(-1) se oprește la linia raise, iese din funcție și caută un except corespunzător în apelant. Dacă niciun apelant nu o capturează, scriptul se încheie cu un traceback.

2.26.1. De ce să generezi o excepție în loc să returnezi o valoare-santinelă

Două moduri de a raporta „input greșit”:

# 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

Forma cu excepție este de obicei mai bună:

  • Apelantul trebuie să trateze în mod deliberat cazul de eroare – fie cu un try, fie lăsând excepția să se propage. Santinelele sunt ușor de uitat și ușor de confundat cu un rezultat normal.

  • Mesajul de eroare călătorește împreună cu excepția; abordarea cu santinelă trebuie să atașeze diagnosticul în altă parte.

  • Comportamentul implicit la o excepție netratată este o blocare zgomotoasă cu un traceback care indică apelul vinovat. Returnările silențioase de None devin mai târziu erori subtile.

Apelează la santinele doar atunci când „nu a fost găsit” este un rezultat de rutină, neexcepțional – dict.get() returnează None la o cheie lipsă tocmai pentru că se așteaptă ca uneori căutările să nu găsească nimic.

2.26.2. Clase de excepții personalizate

Pentru a genera o problemă pe care apelantul ar putea dori să o distingă de erorile încorporate, definește o subclasă a 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)

Corpul class gol este în regulă – numele în sine este ceea ce contează, deoarece apelanții capturează după clasă. Grupează erorile înrudite sub o bază comună dacă un apelant ar putea dori să captureze întreaga familie într-un singur bloc.

2.26.2.1. Re-generarea

Un raise simplu în interiorul unui bloc except re-generează excepția curentă, astfel încât aceasta să se propage la următorul gestionar:

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

Aceasta este forma corectă atunci când o funcție dorește să observe o eroare (să o înregistreze, să o numere, să anuleze o modificare parțială) fără a o trata efectiv.

2.26.3. Când să capturezi și când să propagi

O regulă practică utilă:

  • Capturează o excepție la nivelul care își poate reveni în mod semnificativ – substituie o valoare implicită, reîncearcă, sări peste inputul greșit.

  • Las-o să se propage atunci când nu este nimic util de făcut în afară de blocare, sau când stratul superior este cel care știe cum să își revină.

O funcție aflată la mijlocul unei stive de apeluri care înghite fiecare eroare și returnează silențios face ca eșecurile să fie imposibil de urmărit. Preferă să lași excepțiile să călătorească până ajung la cod care are cu adevărat un plan.