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>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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 e except).

  • 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 chiave except, 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 tryexcept 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 tryexcept.

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>
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>
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>
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>
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>
  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>
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>
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>
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 clausola except. Se l’eccezione non è gestita da una clausola except, l’eccezione viene rilanciata dopo che la clausola finally è stata eseguita.

  • Un’eccezione potrebbe verificarsi durante l’esecuzione di una clausola except o else. Anche in questo caso, l’eccezione viene rilanciata dopo che la clausola finally è stata eseguita.

  • Se la clausola finally esegue un’istruzione break, continue o return, le eccezioni non vengono rilanciate.

  • Se l’istruzione try raggiunge un’istruzione break, continue o return, la clausola finally verrà eseguita appena prima dell’esecuzione dell’istruzione break, continue o return.

  • Se una clausola finally include un’istruzione return, il valore restituito sarà quello dell’istruzione return della clausola finally, non il valore dell’istruzione return della clausola try.

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>
  File "<stdin>", line 3, in divide
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.9. Sollevare e Gestire Eccezioni Multiple e Non Correlate

Ci sono situazioni in cui è necessario segnalare diverse eccezioni che si sono verificate. Questo è spesso il caso nei framework di concorrenza, quando diversi compiti possono aver fallito in parallelo, ma ci sono anche altri casi d’uso in cui è desiderabile continuare l’esecuzione e raccogliere più errori piuttosto che sollevare la prima eccezione.

L’eccezione built-in ExceptionGroup avvolge una lista di istanze di eccezione in modo tale che possano essere sollevate insieme. È un’eccezione a sé stante, quindi può essere catturata come qualsiasi altra eccezione.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |   File "<stdin>", line 3, in f
  | ExceptionGroup: there were problems
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

Usando except* invece di except, possiamo gestire selettivamente solo le eccezioni nel gruppo che corrispondono a un certo tipo. Nell’esempio seguente, che mostra un gruppo di eccezioni annidato, ogni clausola except* estrae dal gruppo le eccezioni di un certo tipo lasciando che tutte le altre eccezioni si propaghino ad altre clausole e, infine, vengano rilanciate.

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |   File "<stdin>", line 2, in f
  | ExceptionGroup: group1
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

Nota che le eccezioni annidate in un gruppo di eccezioni devono essere istanze, non tipi. Questo perché in pratica le eccezioni sarebbero tipicamente quelle che sono state già sollevate e catturate dal programma, seguendo il seguente pattern:

>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

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>
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>
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |   File "<stdin>", line 2, in f
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>