5. Le eccezioni
Passiamo ora alle eccezioni.

5.1. script [exceptions_01]
Il primo script illustra la necessità di gestire le eccezioni.
Si genera intenzionalmente un errore per vedere cosa succede (exceptions-01.py):
La riga 4 ci fornisce:
- il tipo dell'eccezione: ZeroDivisionError;
- il messaggio di errore associato: division by zero. È in inglese. È qualcosa che potremmo voler modificare.
Una regola fondamentale è che bisogna fare di tutto per evitare le eccezioni generate dall’interprete Python. Dobbiamo gestire gli errori da soli.
La sintassi per la gestione delle eccezioni è la seguente:
try:
actions susceptibles de lancer une exception
except (Ex1, Ex2…) as ex:
actions de gestion de l'exception [ex]
except (Ex11, Ex12…) as ex:
actions de gestion de l'exception [ex]
finally:
actions toujours exécutées qu'il y ait exception ou non
Nel blocco try**, l'esecuzione delle azioni si interrompe non appena si verifica un'eccezione (errore). In questo caso, l'esecuzione prosegue con le azioni di una delle clausole **except:
- riga 3: se l’eccezione [ex] che si è verificata è di un tipo appartenente alla tupla (Ex1, Ex2…) o derivato da uno di essi, allora vengono eseguite le azioni della riga 4;
- riga 5: se l’eccezione non è stata intercettata dalla riga 3 e esiste un’altra clausola [except], allora si svolge lo stesso processo. E così via…;
- possono esserci tutte le clausole [except] necessarie per gestire i diversi tipi di eccezione che possono verificarsi nel [try];
- se l’eccezione non è stata gestita da nessuna delle clausole [except], allora verrà segnalata al codice chiamante. Se quest’ultimo si trova a sua volta all’interno di una struttura try/except, l’eccezione viene nuovamente gestita; in caso contrario, continua a risalire la catena dei metodi chiamati. In ultima istanza, l'eccezione arriva all'interprete Python. Quest'ultimo interrompe quindi l'esecuzione del programma e visualizza un messaggio di errore del tipo mostrato nell'esempio precedente. La regola è quindi che il programma principale deve intercettare tutte le eccezioni che possono risalire dai metodi chiamati;
- riga 7: la clausola [finally] viene sempre eseguita, indipendentemente dal fatto che si sia verificata un’eccezione (seguito dell’except) o meno (seguito del try). Ciò vale anche se si è verificata un’eccezione che non è stata intercettata. In questo caso, la clausola [finally] verrà eseguita prima che l’eccezione venga propagata al codice chiamante;
Un'eccezione porta con sé informazioni sull'errore che si è verificato. È possibile ottenerle con la seguente sintassi:
except MyException as exception:
[exception] è l’eccezione che si è verificata. [exception.args] rappresenta la tupla dei parametri dell’eccezione.
Per generare un'eccezione, si utilizza la sintassi
raise MyException(param1, param2…)
dove, nella maggior parte dei casi, MyException è una classe derivata dalla classe BaseException. I parametri passati al costruttore della classe saranno disponibili nella clausola except delle strutture di intercettazione delle eccezioni con la sintassi **[ex.args] se **[ex] è l'eccezione intercettata dalla clausola **[except]**.
Questi concetti sono illustrati dal seguente script.
5.2. script [exceptions_02]
Il seguente script gestisce esplicitamente gli errori:
# gestione delle eccezioni
essai = 0
# si genera un errore e lo si gestisce
x = 2
try:
x = 4 / 0
except ZeroDivisionError as erreur:
# l'errore è l'eccezione intercettata
print(f"essai n° {essai} : {erreur}")
# il valore di x non è cambiato
print(f"x={x}")
# si ricomincia
essai += 1
try:
x = 4 / 0
except BaseException as erreur:
# si intercetta l'eccezione più generica
# l'errore è l'eccezione intercettata
print(f"essai n° {essai} : {erreur}")
# è possibile intercettare diversi tipi di eccezioni
# l'esecuzione si interrompe sul primo [except] in grado di gestire l'eccezione
essai += 1
try:
x = 4 / 0
except ValueError as erreur:
# questa eccezione non si verifica qui
print(f"essai n° {essai} : {erreur}")
except BaseException as erreur:
# viene intercettata l'eccezione più generica
print(f"essai n° {essai} : (Exception) {erreur}")
except ZeroDivisionError as erreur:
# si intercetta un tipo specifico
print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
# si ricomincia cambiando l'ordine
essai += 1
try:
x = 4 / 0
except ValueError as erreur:
# questa eccezione non si verifica qui
print(f"essai n° {essai} : {erreur}")
except ZeroDivisionError as erreur:
# si intercetta un tipo specifico
print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
except BaseException as erreur:
# si intercetta l'eccezione più generica
print(f"essai n° {essai} : (Exception) {erreur}")
# una clausola "except" senza argomenti
essai += 1
try:
x = 4 / 0
except:
# non ci interessa la natura dell'eccezione
print(f"essai n° {essai} : il y a eu un problème")
# un altro tipo di eccezione
essai += 1
try:
# x non può essere convertito in un numero intero
x = int("x")
except ValueError as erreur:
# "errore" è l'eccezione intercettata
print(f"essai n° {essai} : {erreur}")
# un'eccezione trasporta informazioni in una tupla accessibile al programma
essai += 1
try:
x = int("x")
except ValueError as erreur:
# l'errore è l'eccezione intercettata
print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
# È possibile generare eccezioni
essai += 1
try:
raise ValueError("param1", "param2", "param3")
except ValueError as erreur:
# l'errore è l'eccezione intercettata
print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
# è possibile creare le proprie eccezioni
# devono derivare dalla classe [BaseException]
class MyError(BaseException):
pass
# si lancia l'eccezione MyError
essai += 1
try:
raise MyError("info1", "info2", "info3")
except MyError as erreur:
# l'errore è l'eccezione intercettata
print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
# viene generata l'eccezione MyError con un messaggio di errore
essai += 1
try:
raise MyError("mon msg d'erreur")
except MyError as erreur:
# l'errore è l'eccezione intercettata
print(f"essai n° {essai} : {erreur.args[0]}")
# la clausola finally viene sempre eseguita
# indipendentemente dal fatto che si verifichi o meno un'eccezione
essai += 1
x = None
try:
x = 1
except:
# eccezione
print(f"essai n° {essai} : exception")
finally:
# eseguito in ogni caso
print(f"essai n° {essai} : finally x={x}")
essai += 1
x = None
try:
x = 2 / 0
except:
# eccezione
print(f"essai n° {essai} : exception")
finally:
# eseguito in ogni caso
print(f"essai n° {essai} : finally x={x}")
# non è necessario inserire una clausola [except]
essai += 1
try:
# si genera un errore
x = 4 / 0
finally:
# eseguito in ogni caso
print(f"essai n° {essai} : finally x={x}")
Note:
- righe 4-12: si gestisce una divisione per zero;
- riga 8: si intercetta l'eccezione esatta che si verifica e la si visualizza;
- riga 12: a causa dell'eccezione verificatasi, x non ha ricevuto alcun valore alla riga 7 e quindi non ha modificato il proprio valore;
- righe 14-21: si ripete la stessa operazione, ma intercettando un'eccezione di livello superiore di tipo BaseException. Poiché l’eccezione ZeroDivisionError deriva dalla classe BaseException, la clausola except la interromperà;
- righe 23-36: si inseriscono diverse clausole except per gestire diversi tipi di eccezione. Verrà eseguita una sola clausola except oppure nessuna se l’eccezione non soddisfa nessuna clausola except;
- righe 38-50: si ripete la stessa operazione modificando l’ordine delle clausole [except] per illustrarne il funzionamento;
- righe 52-58: la clausola except può non avere alcun argomento. In questo caso, interrompe tutte le eccezioni;
- righe 60-67: introducono l'eccezione ValueError;
- righe 69-75: si recuperano le informazioni trasportate dall'eccezione;
- righe 77-83: illustrano come generare (raise) un'eccezione;
- righe 86-106: illustrano l'utilizzo di una classe di eccezione proprietaria MyError. La classe MyError si limita a derivare dalla classe base BaseException. Non aggiunge nulla alla sua classe base. Tuttavia, ora può essere nominata esplicitamente nelle clausole except;
- righe 108-130: illustrano l'uso della clausola finally;
- righe 132-139: queste righe mostrano che la clausola [except] non è obbligatoria;
I risultati sullo schermo sono i seguenti:
5.3. script [exceptions_03]
Questo nuovo script illustra la propagazione delle eccezioni nella catena delle funzioni chiamanti:
# un'eccezione proprietaria
class MyError(BaseException):
pass
# tre funzioni
def f1(x: int) -> int:
# le eccezioni non vengono gestite: vengono segnalate automaticamente
return f2(x)
def f2(y: int) -> int:
# le eccezioni non vengono gestite: vengono segnalate automaticamente
return f3(y)
def f3(z: int) -> int:
if (z % 2) == 0:
# se z è pari, viene generata un'eccezione
raise MyError("exception dans f3")
else:
return 2 * z
# ---------- main
# le eccezioni risalgono la catena dei metodi chiamati
# finché un metodo non le intercetta. In questo caso sarà main
try:
print(f1(4))
except MyError as erreur:
print(f"type : {type(erreur)}, arguments : {erreur.args}")
# altre tre funzioni che arricchiscono le eccezioni che vengono propagate
def f4(x: int) -> int:
try:
return f5(x)
except MyError as erreur:
# si arricchisce l'eccezione e poi la si rilancia
raise MyError("exception dans f4", erreur)
def f5(y: int) -> int:
try:
return f6(y)
except MyError as erreur:
# si arricchisce l'eccezione e poi la si rilancia
raise MyError("exception dans f5", erreur)
def f6(z: int) -> int:
if (z % 2) == 0:
# si genera un'eccezione se z è pari
raise MyError("exception dans f6")
else:
return 2 * z
# ---------- main
try:
print(f4(4))
except MyError as erreur:
# visualizzazione dell'eccezione
print(f"type : {type(erreur)}, arguments : {erreur.args}")
# è possibile risalire la pila delle eccezioni
err = erreur
# viene visualizzato il messaggio di errore
print(err.args[0])
# un'eccezione è incapsulata?
while len(err.args) == 2 and isinstance(err.args[1], BaseException):
# modifica dell'eccezione
err = err.args[1]
# il primo argomento è il messaggio di errore
print(err.args[0])
Note:
- righe 25-32, nella chiamata main --> f1 --> f2 --> f3 (riga 30), l'eccezione MyError generata da f3 verrà propagata fino a main. Verrà quindi gestita dalla clausola except della riga 31;
- righe 61-75: nella chiamata main --> f4 --> f5 --> f6 (riga 62), l’eccezione MyError generata da f6 risalirà fino a main. Verrà quindi gestita dalla clausola except della riga 63. Questa volta, nel suo percorso risalendo la catena delle funzioni chiamanti, l’eccezione MyError che risale è a sua volta incapsulata in un’altra eccezione;
- righe 66-75: mostrano come risalire lo stack delle eccezioni;
- riga 71: la funzione [isinstance(instance, Classe)] restituisce True se l’oggetto [instance] è di tipo [Classe] o derivato. In questo caso abbiamo utilizzato l’eccezione di livello più alto [BaseException], il che ci garantisce di recuperare tutte le eccezioni;
I risultati visualizzati sullo schermo sono i seguenti: