Eccezioni — gestire ciò che si rompe
Catturare un errore specifico con try/except, perché `except Exception: pass` è quasi sempre sbagliato, `else` e `finally`, sollevare i tuoi errori, e scegliere dalla famiglia un errore della giusta dimensione.
Come vuoi imparare?
Il problema che stiamo risolvendo
L'ultima domanda del capitolo precedente faceva elaborare a un programma tre righe in modo corretto e fermarsi alla quarta. In versione più piccola, è così:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(Prima della riga dell'errore Python stampa anche alcune righe di traceback; in questo capitolo di solito viene mostrata solo l'ultima.)
10 è stato aggiunto correttamente. 30 non è mai stato aggiunto. E print(total) non è mai stato eseguito.
Una riga sbagliata ha fermato tutto il lavoro. Finora abbiamo avuto solo due possibilità — o ogni pezzo di dati è perfetto, oppure il programma si ferma.
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40C'è una terza possibilità: lascia che il problema accada, e poi rispondigli.
Alla fine di questo capitolo sarai in grado di
- Catturare un errore specifico con
try/except - Spiegare perché un
except:nudo o unexcept Exception: passè quasi sempre sbagliato - Afferrare l'errore con
as erre leggerne il messaggio - Dire cosa fanno
elseefinally - Sollevare errori tuoi con
raise, e capire quando farlo - Riconoscere la famiglia degli errori, e catturarne uno della giusta dimensione
Prerequisiti: Lettura e scrittura di file.
Anche un errore è un oggetto
Scrivere as mette l'errore nelle tue mani:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'Il testo rosso che hai visto finora nel terminale sono questi oggetti, e stamparne uno dà il suo messaggio.
C'è una conseguenza pratica: l'errore porta con sé la migliore spiegazione che ci sia. Stampare err è quasi sempre più utile che scrivere "qualcosa è andato storto" con le tue parole.
Cattura l'errore giusto
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'Un except cattura solo il tipo che nomina. Questo nomina TypeError, ma quello che è successo è un ValueError — quindi il blocco except potrebbe anche non esserci.
Può sembrare di ostacolo, ed è proprio la parte utile. La riga except dichiara quale errore avevi previsto — e tutto ciò che non avevi previsto ferma comunque il programma, ed è corretto.
Ecco perché non dovresti scrivere questo:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40La risposta è giusta, ma il codice è comunque brutto. except Exception: pass ingoia tutto — i fallimenti a cui hai pensato e quelli a cui non hai pensato. Quando numbers per caso diventa un dizionario domani, o un nome al suo interno viene scritto male, questo programma darà in silenzio una risposta sbagliata.
La regola: cattura esattamente l'errore per cui hai un piano. E una volta catturato, fai qualcosa — al minimo, dì cosa è successo.
Più di un tipo
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionPiù clausole except possono susseguirsi, e la prima che corrisponde viene eseguita — le altre non vengono nemmeno considerate.
Quando la risposta è la stessa possono essere scritte insieme:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'type(err).__name__ dà il nome del tipo di errore — utile quando si scrivono i log.
Gli errori arrivano in famiglie
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
TrueGli errori sono organizzati come un albero, e catturare un padre cattura i suoi figli:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneQualche relazione che vale la pena conoscere: FileNotFoundError e PermissionError sono entrambi OSError; KeyError e IndexError sono entrambi LookupError; e quasi tutto è un Exception.
Da cui segue la regola: cattura l'errore più piccolo che puoi. except Exception è il più facile da scrivere e il meno utile.
else e finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Noneelse viene eseguito quando nulla nel blocco try si è rotto. Ti permette di tenere l'interno di try piccolo — solo la riga che può fallire — con il lavoro che segue il successo in else. Metti troppo dentro try e catturerai cose che non volevi catturare.
finally viene sempre eseguito, qualunque cosa sia successo. Anche dopo un return:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryIl return ha già deciso il valore, ma finally viene eseguito prima di lasciare la funzione. È il posto per ripulire — anche se per i file, with ha già fatto quel lavoro.
Sollevare i tuoi
Sollevare conta quanto catturare.
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1Qual era l'alternativa a raise? Restituire None, o 0 — e allora l'errore viaggerebbe. Entrerebbe in qualche totale e riemergerebbe molto più tardi come uno strano numero di origine non tracciabile.
Un errore ferma il problema dove è nato, e ne spiega il motivo nel suo messaggio.
Quando scrivi quel messaggio, mettici dentro il valore colpevole. "invalid quantity" e "quantity cannot be negative: -1" — solo il secondo ti permette di iniziare a lavorare.
Un errore può anche essere catturato e rilanciato con un messaggio migliore. parse.py:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'Questa volta viene mostrato l'intero output, perché qui l'ultima riga non racconta tutta la storia. (Sulla tua macchina le righe File conterranno il percorso completo del file; qui è abbreviato con ....) Python ha stampato due traceback: il primo è l'errore originale di int(), il secondo è il nostro nuovo ValueError. La riga al centro, During handling of the above exception, another exception occurred:, dice che il secondo è successo mentre il primo veniva gestito. Nessuna informazione viene persa — ma si legge come se ci fosse un bug dentro il blocco except.
Quando sollevi un nuovo errore apposta, dillo: scrivi except ValueError as err: e poi raise ValueError(f"not a number: {text!r}") from err. La riga al centro diventa allora The above exception was the direct cause of the following exception: — cioè "il primo ha causato il secondo", che è esattamente ciò che vogliamo dire.
{text!r} inserisce il valore tramite repr, quindi le virgolette si vedono — e la differenza tra not a number: 'ten' e not a number: ten conta quando il valore è davvero una stringa vuota o uno spazio di troppo.
Prima chiedi, poi scusati
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedEntrambi sono giusti. Il secondo è più comune in Python, e c'è un motivo: il primo guarda due volte — una in in, una in [...] — e può comunque rompersi se qualcosa cambia nel frattempo.
Per ciò che succede di routine, però, un if si legge meglio. Le eccezioni sono per l'eccezionale; lo dice il nome.
Un esempio completo
orders.txt, con due righe volutamente sbagliate:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py:
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2Quattro cose che val la pena guardare.
parse_line solleva e read_order cattura. La funzione più in basso non ha idea di cosa debba fare il programma nel suo insieme — sa solo che questa riga è inutilizzabile. Decidere che fare spetta alla funzione sopra. Gli errori vengono sollevati in basso e catturati in alto, e quella divisione è tutto il lavoro di un sistema di eccezioni.
Il numero di riga è dentro il messaggio. could not convert string to float: 'eight' non dice dove guardare; line 2: ... lo dice. Tutto ciò che sai nel momento in cui catturi e che non saprai più dopo va nel messaggio.
read_order non si rompe mai. Restituisce due cose — ciò che era utilizzabile e ciò che non lo era. Un file mancante produce una risposta della stessa forma, quindi main non ha bisogno di due percorsi separati.
Il blocco try interno è lungo esattamente una riga. float(price) e int(quantity) — solo la parte che può fallire sta dentro. parts = raw.split(",") è fuori, perché non dovrebbe fallire, e se lo fa voglio saperlo.
Quando si rompe
L'errore non viene catturato, anche se c'è un except Il tipo non corrisponde. La prima parola dell'ultima riga del messaggio è il nome dell'errore — nomina quello, o cattura il suo padre.
C'è un except e il programma si ferma comunque La riga che fallisce non è dentro il blocco try. Controlla l'indentazione.
SyntaxError: expected 'except' or 'finally' block È stato scritto un try senza except. try non può stare da solo.
Tutto "funziona" ma le risposte sono sbagliate C'è un except Exception: pass da qualche parte. Trovalo — è lì che c'è il problema.
Ho sollevato qualcosa ma non compare alcun messaggio È stato scritto raise ValueError invece di raise ValueError("..."). Aggiungi le parentesi e il messaggio.
Un return dentro finally fa perdere l'errore Sì, ed è per questo che non dovresti scriverne uno lì. finally serve solo a ripulire.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
C'è un except. Cosa succede al programma?
try:
int("twenty")
except TypeError:
print("caught")- AViene stampato `caught`
- BNon viene stampato nulla e il programma finisce in silenzio
- CSi ferma con un `TypeError`
- DIl programma si ferma con un `ValueError` — l'`except` potrebbe anche non esserci
L'intero ciclo è dentro il try. Cosa viene stampato?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
Sia il try che l'except hanno un return. Cosa viene stampato?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Torna al summary.py del capitolo precedente, che leggeva people.txt.
Ora fai in modo che non si schianti mai, e che finisca dicendo quali righe sono state saltate e perché. Scrivi un parse_line(raw, number) che solleva un ValueError con dentro il numero di riga quando la riga è sbagliata.
Poi danneggia il file apposta — togli una virgola da una riga, metti lettere dove in un'altra ci va un numero, e lascia una riga del tutto vuota. Per esempio:
rafi,12
nadia 30
sami,ten
lina,25La riga vuota non è un reclamo — come nel capitolo precedente, viene saltata in silenzio. Quindi il programma deve arrivare alla fine e riportare due reclami. Se scrivi i messaggi come fa l'esempio completo (expected 2 fields, got ... e line {number}: {err}), la fine dell'output sarà esattamente questa:
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'Poi quattro esperimenti:
- Sostituisci
except ValueErrorconexcept Exception: pass. Qual è l'output, e perché è più pericoloso? - Togli il numero di riga dal messaggio di
parse_line. Quanto tempo serve ora per trovare una riga sbagliata? - Ingrossa il blocco
try— mettici dentro l'intero corpo del ciclo. Cosa succede ora quando una riga è sbagliata? - Aggiungi un
finallyche stampa qualcosa dopo ogni riga, e verifica che venga eseguito tanto sulle buone quanto sulle cattive.
Il terzo esperimento insegna l'abitudine più utile di questo capitolo: quanto più piccolo è il blocco try, tanto più onesta è la cattura.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz