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" 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 "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 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>
       divide("2", "0")
       ~~~~~~^^^^^^^^^^
     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.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>
     |     f()
     |     ~^^
     |   File "<stdin>", line 3, in f
     |     raise ExceptionGroup('there were problems', excs)
     | ExceptionGroup: there were problems (2 sub-exceptions)
     +-+---------------- 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>
     |     f()
     |     ~^^
     |   File "<stdin>", line 2, in f
     |     raise ExceptionGroup(
     |     ...<12 lines>...
     |     )
     | ExceptionGroup: group1 (1 sub-exception)
     +-+---------------- 1 ----------------
       | ExceptionGroup: group2 (1 sub-exception)
       +-+---------------- 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>
       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
       +------------------------------------
   >>>
