8. Errori ed Eccezioni¶
Finora i messaggi di errore sono stati solo menzionati, ma se hai provato gli esempi probabilmente ne hai visti alcuni. Ci sono (almeno) due tipi distinti di errori: errori di sintassi ed eccezioni.
8.1. Errori di Sintassi¶
Gli errori di sintassi, noti anche come errori di parsing, sono forse il tipo più comune di reclamo che si ottiene mentre si sta ancora imparando Python:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
Il parser ripete la linea incriminata e mostra delle piccole “freccette” che puntano al token nella linea in cui è stato rilevato l’errore. L’errore può essere causato dall’assenza di un token prima del token indicato. Nell’esempio, l’errore è rilevato alla funzione print()
, poiché manca un due punti (':'
) prima di essa. Vengono stampati il nome del file e il numero di linea in modo che si sappia dove cercare nel caso in cui l’input provenga da uno script.
8.2. Eccezioni¶
Anche se un’istruzione o un’espressione è sintatticamente corretta, può causare un errore quando si tenta di eseguirla. Gli errori rilevati durante l’esecuzione sono chiamati eccezioni e non sono incondizionatamente fatali: imparerai presto come gestirli nei programmi Python. La maggior parte delle eccezioni non viene gestita dai programmi, tuttavia, e si traduce in messaggi di errore come mostrato qui:
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
4 + spam*3
^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'2' + 2
~~~~^~~
TypeError: can only concatenate str (not "int") to str
L’ultima linea del messaggio di errore indica cosa è successo. Le eccezioni hanno tipi diversi, e il tipo viene stampato come parte del messaggio: i tipi nell’esempio sono ZeroDivisionError
, NameError
e TypeError
. La stringa stampata come tipo di eccezione è il nome dell’eccezione built-in che si è verificata. Questo è vero per tutte le eccezioni built-in, ma non deve essere vero per le eccezioni definite dall’utente (anche se è una convenzione utile). I nomi delle eccezioni standard sono identificatori built-in (non parole chiave riservate).
La restante parte della linea fornisce dettagli basati sul tipo di eccezione e su cosa l’ha causata.
La parte precedente del messaggio di errore mostra il contesto in cui si è verificata l’eccezione, sotto forma di traccia dello stack. In generale, contiene una traccia dello stack che elenca le linee di origine; tuttavia, non visualizzerà linee lette dall’input standard.
Built-in Exceptions elenca le eccezioni built-in e i loro significati.
8.3. Gestione delle Eccezioni¶
È possibile scrivere programmi che gestiscono eccezioni selezionate. Guarda il seguente esempio, che chiede all’utente un input finché non viene inserito un intero valido, ma consente all’utente di interrompere il programma (usando Control-C o cosa supporta il sistema operativo); nota che un’interruzione generata dall’utente è segnalata sollevando l’eccezione KeyboardInterrupt
.
>>> while True:
... try:
... x = int(input("Please enter a number: "))
... break
... except ValueError:
... print("Oops! That was no valid number. Try again...")
...
L’istruzione try
funziona come segue.
Per prima cosa, viene eseguita la clausola try (l’istruzione o le istruzioni tra le parole chiave
try
eexcept
).Se non si verifica alcuna eccezione, la clausola except viene saltata e l’esecuzione dell’istruzione
try
è terminata.Se si verifica un’eccezione durante l’esecuzione della clausola
try
, il resto della clausola viene saltato. Poi, se il suo tipo corrisponde all’eccezione nominata dopo la parola chiaveexcept
, viene eseguita la clausola except, e quindi l’esecuzione continua dopo il blocco try/except.Se si verifica un’eccezione che non corrisponde all’eccezione nominata nella clausola except, viene passata alle istruzioni
try
esterne; se non viene trovato alcun gestore, è un” eccezione non gestita e l’esecuzione si interrompe con un messaggio di errore.
Un’istruzione try
può avere più di una clausola except, per specificare gestori per diverse eccezioni. Al massimo verrà eseguito un gestore. I gestori gestiscono solo le eccezioni che si verificano nella corrispondente clausola try, non in altri gestori della stessa istruzione try
. Una clausola except può nominare più eccezioni come una tupla tra parentesi, per esempio:
... except (RuntimeError, TypeError, NameError):
... pass
Una classe in una clausola except
corrisponde a eccezioni che sono istanze della classe stessa o di una delle sue classi derivate (ma non viceversa — una clausola except che elenca una classe derivata non corrisponde a istanze delle sue classi base). Ad esempio, il seguente codice stamperà B, C, D in quell’ordine:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
Nota che se le clausole except fossero invertite (con except B
prima), avrebbero stampato B, B, B — la prima clausola except che corrisponde viene attivata.
Quando si verifica un’eccezione, essa può avere valori associati, noti anche come argomenti dell’eccezione. La presenza e il tipo degli argomenti dipendono dal tipo di eccezione.
La clausola except può specificare una variabile dopo il nome dell’eccezione. La variabile è legata all’istanza dell’eccezione che tipicamente ha un attributo args
che memorizza gli argomenti. Per comodità, i tipi di eccezioni built-in definiscono __str__()
per stampare tutti gli argomenti senza accedere esplicitamente a .args
.
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # the exception type
... print(inst.args) # arguments stored in .args
... print(inst) # __str__ allows args to be printed directly,
... # but may be overridden in exception subclasses
... x, y = inst.args # unpack args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
L’output di __str__()
dell’eccezione viene stampato come l’ultima parte (“dettaglio”) del messaggio per le eccezioni non gestite.
BaseException
è la classe base comune di tutte le eccezioni. Uno dei suoi sottosclassi, Exception
, è la classe base di tutte le eccezioni non fatali. Le eccezioni che non sono sottoclassi di Exception
non sono tipicamente gestite, perché vengono utilizzate per indicare che il programma dovrebbe terminare. Queste includono SystemExit
che è sollevata da sys.exit()
e KeyboardInterrupt
che è sollevata quando un utente desidera interrompere il programma.
Exception
può essere utilizzata come un jolly che cattura (quasi) tutto. Tuttavia, è buona pratica essere il più specifici possibile con i tipi di eccezioni che intendiamo gestire, e permettere a qualsiasi eccezione inattesa di propagarsi.
Il pattern più comune per la gestione di Exception
è stamparla o registrarla e poi rilanciarla (consentendo a un chiamante di gestire l’eccezione altrettanto):
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error:", err)
except ValueError:
print("Could not convert data to an integer.")
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
L’istruzione try
… except
ha una clausola else opzionale, che, quando presente, deve seguire tutte le clausole except. È utile per il codice che deve essere eseguito se la clausola try non solleva un’eccezione. Per esempio:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
L’uso della clausola else
è migliore che aggiungere codice ulteriore alla clausola try
perché evita di catturare accidentalmente un’eccezione che non è stata sollevata dal codice protetto dall’istruzione try
… except
.
I gestori delle eccezioni non gestiscono solo le eccezioni che si verificano immediatamente nella clausola try, ma anche quelle che si verificano all’interno delle funzioni che vengono chiamate (anche indirettamente) nella clausola try. Per esempio:
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('Handling run-time error:', err)
...
Handling run-time error: division by zero
8.4. Sollevare Eccezioni¶
L’istruzione raise
permette al programmatore di forzare il verificarsi di una specifica eccezione. Per esempio:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('HiThere')
NameError: HiThere
L’unico argomento per raise
indica l’eccezione da sollevare. Questo deve essere o un’istanza di eccezione o una classe di eccezione (una classe che deriva da BaseException
, come Exception
o una delle sue sottoclassi). Se viene passata una classe di eccezione, verrà istanziata implicitamente chiamando il suo costruttore senza argomenti:
raise ValueError # shorthand for 'raise ValueError()'
Se è necessario determinare se un’eccezione è stata sollevata ma non si intende gestirla, una forma più semplice dell’istruzione raise
consente di rilanciare l’eccezione:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('An exception flew by!')
... raise
...
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('HiThere')
NameError: HiThere
8.5. Collegamento delle Eccezioni¶
Se si verifica un’eccezione non gestita all’interno di una sezione except
, avrà l’eccezione che viene gestita allegata e inclusa nel messaggio di errore:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("database.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error
Per indicare che un’eccezione è una diretta conseguenza di un’altra, l’istruzione raise
permette una clausola opzionale from
:
# exc must be exception instance or None.
raise RuntimeError from exc
Questo può essere utile quando si stanno trasformando le eccezioni. Per esempio:
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
func()
~~~~^^
File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database
Consente anche di disabilitare il collegamento automatico delle eccezioni utilizzando l’idioma from None
:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
Per ulteriori informazioni sulla meccanica del collegamento, vedi Built-in Exceptions.
8.6. Eccezioni Definite dall’Utente¶
I programmi possono nominare le proprie eccezioni creando una nuova classe di eccezione (vedi Classi per ulteriori informazioni sulle classi Python). Le eccezioni devono essere tipicamente derivate dalla classe Exception
, direttamente o indirettamente.
Si possono definire classi di eccezione che fanno qualsiasi cosa possa fare qualsiasi altra classe, ma di solito vengono mantenute semplici, spesso offrendo solo un numero di attributi che consentono di estrarre informazioni sull’errore dai gestori per l’eccezione.
La maggior parte delle eccezioni è definita con nomi che terminano in «Error», simili alla denominazione delle eccezioni standard.
Molti moduli standard definiscono le proprie eccezioni per segnalare errori che possono verificarsi nelle funzioni che definiscono.
8.7. Definizione di Azioni di Pulizia¶
L’istruzione try
ha un’altra clausola opzionale che è intesa a definire azioni di pulizia che devono essere eseguite in tutte le circostanze. Per esempio:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
Se è presente una clausola finally
, la clausola finally
verrà eseguita come ultimo compito prima che l’istruzione try
completi. La clausola finally
viene eseguita sia che l’istruzione try
produca o meno un’eccezione. I seguenti punti discutono casi più complessi quando si verifica un’eccezione:
Se si verifica un’eccezione durante l’esecuzione della clausola
try
, l’eccezione può essere gestita da una clausolaexcept
. Se l’eccezione non è gestita da una clausolaexcept
, l’eccezione viene rilanciata dopo che la clausolafinally
è stata eseguita.Un’eccezione potrebbe verificarsi durante l’esecuzione di una clausola
except
oelse
. Anche in questo caso, l’eccezione viene rilanciata dopo che la clausolafinally
è stata eseguita.Se la clausola
finally
esegue un’istruzionebreak
,continue
oreturn
, le eccezioni non vengono rilanciate.Se l’istruzione
try
raggiunge un’istruzionebreak
,continue
oreturn
, la clausolafinally
verrà eseguita appena prima dell’esecuzione dell’istruzionebreak
,continue
oreturn
.Se una clausola
finally
include un’istruzionereturn
, il valore restituito sarà quello dell’istruzionereturn
della clausolafinally
, non il valore dell’istruzionereturn
della clausolatry
.
Per esempio:
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
Un esempio più complicato:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
divide("2", "1")
~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in divide
result = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
Come puoi vedere, la clausola finally
viene eseguita in ogni caso. L’eccezione TypeError
sollevata dividendo due stringhe non è gestita dalla clausola except
e quindi rilanciata dopo che la clausola finally
è stata eseguita.
Nelle applicazioni del mondo reale, la clausola finally
è utile per rilasciare risorse esterne (come file o connessioni di rete), indipendentemente dal successo dell’uso delle risorse.
8.8. Azioni di Pulizia Predefinite¶
Alcuni oggetti definiscono azioni di pulizia standard da intraprendere quando l’oggetto non è più necessario, indipendentemente dal fatto che l’operazione che usa l’oggetto sia riuscita o fallita. Guarda il seguente esempio, che tenta di aprire un file e stampare il suo contenuto sullo schermo.
for line in open("myfile.txt"):
print(line, end="")
Il problema con questo codice è che lascia il file aperto per un tempo indeterminato dopo che questa parte del codice ha terminato l’esecuzione. Questo non è un problema negli script semplici, ma può essere un problema per applicazioni più grandi. L’istruzione with
consente agli oggetti come i file di essere usati in modo tale da garantire che siano sempre puliti rapidamente e correttamente.
with open("myfile.txt") as f:
for line in f:
print(line, end="")
Dopo che l’istruzione è stata eseguita, il file f è sempre chiuso, anche se si è verificato un problema durante l’elaborazione delle linee. Gli oggetti che, come i file, forniscono azioni di pulizia predefinite indicheranno ciò nella loro documentazione.
8.10. Arricchire le Eccezioni con Note¶
Quando un’eccezione viene creata per essere sollevata, viene solitamente inizializzata con informazioni che descrivono l’errore verificatosi. Ci sono casi in cui è utile aggiungere informazioni dopo che l’eccezione è stata catturata. Per questo scopo, le eccezioni hanno un metodo add_note(note)
che accetta una stringa e la aggiunge alla lista di note dell’eccezione. Il rendering standard del traceback include tutte le note, nell’ordine in cui sono state aggiunte, dopo l’eccezione.
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.add_note('Add some information')
... e.add_note('Add some more information')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>
Per esempio, quando si raccolgono eccezioni in un gruppo di eccezioni, potremmo voler aggiungere informazioni di contesto per gli errori individuali. Nel seguente esempio, ogni eccezione nel gruppo ha una nota che indica quando si è verificato l’errore.
>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Happened in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('We have some problems', excs)
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>