Articolo originale: How to Handle Exceptions in Python: A Detailed Visual Introduction

Benvenuto! In questo articolo imparerai come gestire eccezioni in Python.

Nel dettaglio, tratteremo di:

  • Eccezioni
  • Lo scopo della gestione delle eccezioni
  • La clausola try
  • La clausola except
  • La clausola else
  • La clausola finally
  • Come generare eccezioni

Sei pronto? Iniziamo! 😀

1️⃣ Introduzione alle Eccezioni

Partiamo dalle eccezioni:

  • Cosa sono?
  • Perché sono rilevanti?
  • Perché dovresti gestirle?

Secondo la documentazione di Python:

Gli errori rilevati durante l'esecuzione sono chiamati eccezioni e non sono incondizionatamente fatali.

Le eccezioni vengono generate quando un programma incontra un errore durante la sua esecuzione. Interrompono il normale flusso del programma che in genere finisce bruscamente. Per evitare ciò, puoi intercettarle e gestirle in modo appropriato.

Avrai probabilmente visto eccezioni durante lo sviluppo dei tuoi progetti di programmazione.

Se hai mai provato a dividere un numero per zero in Python, avrai visto questo messaggio di errore:

>>> a = 5/0
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    a = 5/0
ZeroDivisionError: division by zero
# ZeroDivisionError: divisione per zero

Se hai cercato di ottenere un carattere da una stringa indicando un valore di indice non valido, sicuramente hai ottenuto questo messaggio di errore:

>>> a = "Hello, World"
>>> a[456]
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    a[456]
IndexError: string index out of range
# IndexError: indice stringa fuori intervallo

Questi sono esempi di eccezioni.

🔹 Eccezioni Comuni

Ci sono molti tipi diversi di eccezioni, che sono tutte generate in caso di situazioni particolari. Alcune delle eccezioni che più probabilmente incontrerai quando lavori ai tuoi progetti sono:

  • IndexError (Errore di indice) - sollevata quando cerchi di utilizzare un indice per liste, tuple o stringhe che va oltre i limiti permessi. Per esempio:
>>> num = [1, 2, 6, 5]
>>> num[56546546]
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    num[56546546]
IndexError: list index out of range
# IndexError: indice della lista fuori intervallo
  • KeyError (Errore di chiave) - sollevata quanto cerchi di accedere al valore di una chiave che non esiste in un dizionario. Per esempio:
>>> students = {"Nora": 15, "Gino": 30}
>>> students["Lisa"]
Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    students["Lisa"]
KeyError: 'Lisa'
  • NameError (Errore di nome) - sollevata quando un nome a cui ti stai riferendo nel codice non esiste. Per esempio:
>>> a = b
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    a = b
NameError: name 'b' is not defined
# NameError: il nome 'b' non è definito
  • TypeError (Errore di tipo) - sollevata quando un'operazione o una funzione viene applicata a un oggetto del tipo non appropriato. Per esempio:
>>> (5, 6, 7) * (1, 2, 3)
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    (5, 6, 7) * (1, 2, 3)
TypeError: can't multiply sequence by non-int of type 'tuple'
# TypeError: impossibile moltiplicare una sequenza per un non intero di tipo 'tuple'
  • ZeroDivisionError (Errore di divisione per zero) - sollevata quando cerchi di dividere per zero. Per esempio:
>>> a = 5/0
Traceback (most recent call last):
  File "<pyshell#13>", line 1, in <module>
    a = 5/0
ZeroDivisionError: division by zero
# ZeroDivisionError: divisione per zero

💡 Suggerimento: Per saperne di più su altri tipi di eccezione integrati, fai riferimento a questo articolo (in lingua inglese) nella documentazione di Python.

🔸 Anatomia di un'Eccezione

Avrai sicuramente notato un modello generale per questi messaggi di errore. Analizziamo questa struttura generale nelle sue parti:

image-8

Per prima cosa troviamo questa riga (vedi sotto). Un traceback è in pratica un elenco dettagliato delle chiamate di funzione che sono state effettuate prima che l'eccezione venisse generata.

Il traceback ti aiuta nel processo di debug, in quanto puoi analizzare la sequenza di chiamate alle funzioni che hanno generato l'eccezione:

Traceback (most recent call last):

Poi abbiamo questa riga (vedi sotto) con il percorso al file, il numero di riga e il codice della riga che ha fatto sollevare l'eccezione. In questo caso il percorso è la shell di Python <pyshell#0> visto che l'esempio è stato eseguito direttamente in IDLE.

File "<pyshell#0>", line 1, in <module>
   a - 5/0

💡 Suggerimento: Se la riga che ha fatto sollevare l'eccezione è in una funzione,  <module> viene sostituito dal nome della funzione.

Alla fine troviamo un messaggio descrittivo con il dettaglio del tipo di eccezione e sono fornite informazioni aggiuntive per aiutarci nel debug del codice:

NameError: name 'a' is not defined

2️⃣ Gestione Eccezioni: Scopo e Contesto

Potresti chiedere: perché dovrei voler gestire eccezioni? In che modo mi sarebbe utitle? Gestendo le eccezioni, puoi fornire un flusso di esecuzione alternativo per evitare che il tuo programma si arresti inaspettatamente.

🔹 Esempio: Input Utente

Immagina cosa accadrebbe se un utente che sta lavorando con il tuo programma digitasse un input non valido. Verrebbe generata un'eccezione in quanto durante il processo è stata eseguita un'operazione non valida.

Se il tuo programma non gestisce l'input correttamente, si arresterà improvvisamente e l'utente avrà una deludente esperienza utilizzando il tuo prodotto.

Ma se gestisci l'eccezione, sarai in grado di fornire un'alternativa per migliorare l'esperienza dell'utente.

Forse potresti mostrare un messaggio descrittivo chiedendo all'utente di digitare un input valido o potresti usare un valore predefinito per l'input. A seconda del contesto, puoi scegliere cosa fare quando succede, e questa è la magia della gestione degli errori. Ti può salvare quando accade l'imprevisto. ⭐️

🔸 Cosa succede dietro le quinte?

Fondamentalmente, quando gestiamo un'eccezione, stiamo dicendo al programma cosa fare se l'eccezione viene generata. In tal caso, il flusso di esecuzione "alternativo" verrà in soccorso. Se non vengono sollevate eccezioni, il codice verrà eseguito come previsto.

image-10

3️⃣ È Ora di Scrivere del Codice: l'Istruzione try ... except

Ora che sai cosa sono le eccezioni e perché dovresti gestirle, iniziamo a conoscere gli strumenti integrati che Python offre allo scopo.

Innanzitutto abbiamo l'istruzione base: try ... except.

Illustriamola con un semplice esempio. Abbiamo questo piccolo programma che chiede all'utente di digitare il nome di uno studente per visualizzare la sua età:

students = {"Nora": 15, "Gino": 30}

def print_student_age():
    name = input("Per favore digita il nome dello studente: ")
    print(students[name])

print_student_age()

Nota come non stiamo validando l'input utente al momento, quindi l'utente potrebbe digitare nomi non validi (nomi che non sono presenti nel dizionario students), le conseguenze sarebbero catastrofiche, in quanto il programma si arresterebbe in caso di errore di chiave KeyError:

# Input Utente
Per favore digita il nome dello studente: Daniel

# Messaggio di errore
Traceback (most recent call last):
  File "<path>", line 15, in <module>
    print_student_age()
  File "<path>", line 13, in print_student_age
    print(students[name])
KeyError: 'Daniel'

🔹 Sintassi

Possiamo gestire bene questa eccezione usando try ... except. Ecco la sintassi base:

image-11

Nel nostro esempio, aggiungeremo un'istruzione try ... except all'interno della funzione. Analizziamo il codice in dettaglio:

students = {"Nora": 15, "Gino": 30}

def print_student_age():
    while True:
        try:
            name = input("Per favore digita il nome dello studente: ")
            print(students[name])
            break
        except:
            print("Questo nome non risulta registrato")
    

print_student_age()

Concentriamoci sull'istruzione  try ... except:

try:
	name = input("Per favore digita il nome dello studente: ")
	print(students[name])
	break
except:
	print("Questo nome non risulta registrato")
  • Quando la funzione viene chiamata, viene eseguita la clausola try. Se non sono sollevate eccezioni, il programma viene eseguito come previsto.
  • Se viene sollevata un'eccezione all'interno della clausola try, il flusso di esecuzione passa immediatamente al contenuto della clausola except per la gestione dell'eccezione.

💡 Nota: Questo codice si trova all'interno di un ciclo while per continuare a chiedere all'utente un valore se lo stesso non è valido. Ecco un esempio:

Per favore digita il nome dello studente: Lulu
Questo nome non risulta registrato
Per favore digita il nome dello studente: 

Ottimo, giusto? Ora possiamo continuare a richiedere un input all'utente se questo non è valido.

Al momento stiamo gestendo tutte le eccezioni possibili con la stessa clausola except. Se volessimo invece voler gestire uno specifico tipo di eccezione? Vediamo come farlo.

🔸 Rilevare Eccezioni Specifiche

Visto che non tutti i tipi di eccezione sono gestiti allo stesso modo, possiamo specificare quali eccezioni vorremmo gestire con questa sintassi:

image-15

Questo è un esempio. Gestiamo una eccezione di tipo ZeroDivisionError nel caso l'utente digiti zero come denominatore:

def divide_integers():
    while True:
        try:
            a = int(input("Per favore digita il numeratore: "))
            b = int(input("Per favore digita il denominatore: "))
            print(a / b)
        except ZeroDivisionError:
            print("Per favore digita un denominatore valido.")


divide_integers()

Questo sarebbe il risultato:

# Prima iterazione
Per favore digita il numeratore: 5
Per favore digita il denominatore: 0
Per favore digita un denominatore valido. 

# Seconda iterazione
Per favore digita il numeratore: 5
Per favore digita il denominatore: 2
2.5

Stiamo gestendo correttamente l'eccezione. Tuttavia se venisse sollevata un'eccezione di tipo diverso il programma non la gestirebbe bene.

Ecco un esempio di un errore ValueError, perché uno dei valori è un decimale, non un intero:

Per favore digita il numeratore: 5
Per favore digita il denominatore: 0.5
Traceback (most recent call last):
  File "<path>", line 53, in <module>
    divide_integers()
  File "<path>", line 47, in divide_integers
    b = int(input("Per favore digita il denominatore: "))
ValueError: invalid literal for int() with base 10: '0.5'
# ValueError: valore letterale non valido per int() con base 10: '0.5'

Possiamo personalizzare il modo con il quale gestiamo diversi tipi di eccezione.

🔹 Clausole Except Multiple

Per fare questo dobbiamo aggiungere ulteriori clausole except per gestire tipi di eccezione differenti in modo diverso.

Secondo la documentazione di Python:

Un'istruzione try può avere più di una clausola except, per specificare gestori per eccezioni diverse. Non più di un gestore verrà eseguito.

In questo esempio abbiamo due clausole except. Una di esse gestisce l'errore ZeroDivisionError e l'altra l'errore ValueError, i due tipi di eccezione che potrebbero essere sollevate in questo blocco try.

def divide_integers():
    while True:
        try:
            a = int(input("Per favore digita il numeratore: "))
            b = int(input("Per favore digita il denominatore: "))
            print(a / b)
        except ZeroDivisionError:
            print("Per favore digita un denominatore valido.")
        except ValueError:
            print("Entrambi i valori devono essere interi.")


divide_integers() 

💡 Suggerimento: Spetta a te determinare quali tipi di eccezione potrebbero essere sollevate in un blocco try per gestirle in modo appropriato.

🔸 Eccezioni Multiple, Una Clausola Except

Puoi anche scegliere di gestire diversi tipi di eccezioni con la stessa clausola except.

Secondo la documentazione di Python:

Una clausola except può includere eccezioni multiple inserite in una tupla fra parentesi.

Nell'esempio che segue intercettiamo due eccezioni (ZeroDivisionError e ValueError) con la stessa clausola except:

def divide_integers():
    while True:
        try:
            a = int(input("Per favore digita il numeratore: "))
            b = int(input("Per favore digita il denominatore: "))
            print(a / b)
        except (ZeroDivisionError, ValueError):
            print("Per favore digita interi validi.")

divide_integers()

Il risultato sarebbe lo stesso per due tipi di eccezione, in quanto sono gestiti dalla stessa clausola:

Per favore digita il numeratore: 5
Per favore digita il denominatore: 0
Per favore digita interi validi.
Per favore digita il numeratore: 0.5
Per favore digita interi validi.
Per favore digita il numeratore: 

🔹 Gestione delle Eccezioni Sollevate da Funzioni Chiamate nella Clausola try

Un aspetto interessante della gestione delle eccezioni è che se un'eccezione viene sollevata in una funzione che era stata precedentemente chiamata all'interno della clausola try di un'altra funzione, e la funzione stessa non la gestisce, il chiamante la potrà gestire se c'è una clausola except appropriata.

Secondo la documentazione di Python:

I gestori di eccezione non solo gestiscono eccezioni se si verificano immediatamente nella clausola try, ma anche se si verificano all'interno di funzioni che sono state chiamate (anche indirettamente) nella clausola try.

Ecco un esempio per illustrare quanto sopra:

def f(i):
    try:
        g(i)
    except IndexError:
        print("Per favore digita un indice valido")

def g(i):
    a = "Ciao"
    return a[i]

f(50)

Abbiamo la funzione  f e la funzione g . f chiama g nella clausola try. Con l'argomento 50, g solleverà una eccezione IndexError in quanto l'indice 50 non è valido per la stringa a.

Tuttavia g stessa non gestisce l'eccezione. Nota come non c'è un'istruzione try ... except nella funzione g. Visto che essa non gestisce l'eccezione, la "spedisce" alla funzione f per vedere se la può gestire, come puoi vedere nel diagramma seguente:

image-16

Visto che f sa come trattare un'eccezione IndexError, la situazione è ben gestita e questo è il risultato:

Per favore digita un indice valido

💡 Nota: Se f non avesse gestito l'eccezione, il programma si sarebbe arrestato bruscamente con il messaggio di errore predefinito per una eccezione IndexError.

🔸 Accedere a Dettagli Specifici delle Eccezioni

In Python le eccezioni sono oggetti, quindi puoi assegnare l'eccezione che è stata sollevata a una variabile. In questo modo puoi stampare la descrizione predefinita dell'eccezione e accedere ai suoi argomenti.

Secondo la documentazione di Python:

La clausola except può specificare una variabile dopo il nome dell'eccezione. La variabile è associata a una istanza dell'eccezione con gli argomenti conservati in istanza.args.

Di seguito un esempio dove assegniamo un'istanza di eccezione ZeroDivisionError alla variabile e. Successivamente, possiamo usare questa variabile all'interno di un clausola except per accedere al tipo di eccezione, al suo messaggio e ai suoi argomenti.

def divide_integers():
    while True:
        try:
            a = int(input("Per favore digita il numeratore: "))
            b = int(input("Per favore digita il denominatore: "))
            print(a / b)
        # Qui assegniamo l'eccezione alla variabile e
        except ZeroDivisionError as e:
            print(type(e))
            print(e)
            print(e.args)

divide_integers()

Il risultato sarà:

Per favore digita il numeratore: 5
Per favore digita il denominatore: 0

# Tipo
<class 'ZeroDivisionError'>

# Messaggio
division by zero

# Argomenti
('division by zero',)

💡 Suggerimento: se hai familiarità con i metodi speciali, secondo la documentazione di Python: "per convenienza, l'istanza dell'eccezione definisce __str__() in modo che gli argomenti possano essere stampati direttamente senza dover fare riferimento a .args."

4️⃣ Ora Aggiungiamo la Clausola "else"

La clausola else è opzionale, ma è un ottimo strumento dato che consente di eseguire del codice che dovrebbe essere eseguito solo se non vengono sollevate eccezioni nella clausola try.

image-17

In base alla documentazione di Python:

L'istruzione tryexcept ha una clausola else opzionale, la quale, se presente, deve seguire tutte le clausole except. È utile per del codice che deve essere eseguito se la clausola try non solleva una eccezione.

Ecco un esempio di utilizzo della clausola else:

def divide_integers():
    while True:
        try:
            a = int(input("Per favore digita il numeratore: "))
            b = int(input("Per favore digita il denominatore: "))
            result = a / b
        except (ZeroDivisionError, ValueError):
            print("Per favore digita interi validi. Il denominatore non può essere zero")
        else:
            print(result)

divide_integers()

Se non sono state sollevate eccezioni, viene stampato il risultato dell'operazione:

Per favore digita il numeratore: 5
Per favore digita il denominatore: 5
1.0

Se viene sollevata una eccezione il risultato non viene stampato:

Please enter the numerator: 5
Please enter the denominator: 0
Per favore digita interi validi. Il denominatore non può essere zero

💡 Suggerimento: secondo la documentazione Python:

Usare la clausola else è meglio che aggiungere codice addizionale alla clausola try in quanto evita che vengano accidentalmente catturate eccezioni che non sono state sollevate del codice protetto dall'istruzione  tryexcept.

5️⃣ La clausola "finally"

La clausola finally è l'ultima clausola in questa sequenza. È opzionale, ma se la includi, deve essere l'ultima clausola della sequenza. La clausola finally viene sempre eseguita, anche se viene sollevata una eccezione dalla clausola try.

image-19

Secondo la documentazione di Python:

Se è presente una clausola finally, essa verrà eseguita come ultima attività prima del completamento dell'istruzione try. La clausola finally viene eseguita a prescindere da eccezioni prodotte dall'istruzione try.

La clausola finally viene in genere usata per eseguire operazioni di "pulizia" che dovrebbero sempre essere completate. Per esempio, se stiamo lavorando su un file nella clausola try, dovremo sempre chiudere il file, anche se viene sollevata una eccezione nella clausola try mentre stiamo lavorando con i dati.

Ecco un esempio della clausola finally:

def divide_integers():
    while True:
        try:
            a = int(input("Per favore digita il numeratore: "))
            b = int(input("Per favore digita il denominatore: "))
            result = a / b
        except (ZeroDivisionError, ValueError):
            print("Per favore digita interi validi. Il denominatore non può essere zero")
        else:
            print(result)
        finally:
            print("All'interno della clausola finally")

divide_integers()

Ecco il risultato quando non viene sollevata nessuna eccezione

Per favore digita il numeratore: 5
Per favore digita il denominatore: 5
1.0
All'interno della clausola finally

Ecco il risultato quando viene sollevata un'eccezione:

Per favore digita il numeratore: 5
Per favore digita il denominatore: 0
Per favore digita interi validi. Il denominatore non può essere zero
All'interno della clausola finally

Nota come la clausola finally viene sempre eseguita.

❗️Importante: ricorda che le clausole else e finally sono opzionali, ma se decidi di includerle entrambe, la clausola finally deve essere l'ultima della sequenza.

6️⃣ Sollevare Eccezioni

Ora che sai come gestire le eccezioni in Python, vorrei condividere con te questo utile suggerimento: puoi anche scegliere quando sollevare eccezioni nel tuo codice.

Può essere utile in alcune situazioni. Vediamo come possiamo fare:

image-20

Questa riga solleverà una eccezione ValueError con un messaggio personalizzato.

Di seguito abbiamo un esempio di una funzione che stampa il valore degli elementi di una lista o una tupla, oppure i caratteri in una stringa. Decidi che vuoi che la lista, la tupla o la stringa debba avere una lunghezza di 5. Fai partire la funzione con un'istruzione if che verifica che la lunghezza dell'argomento data sia 5. In caso contrario viene sollevata un'eccezione ValueError:

def print_five_items(data):
    
    if len(data) != 5:
        raise ValueError("L'argomento deve avere cinque elementi")
    
    for item in data:
        print(item)

print_five_items([5, 2])

Il risultato sarebbe:

Traceback (most recent call last):
  File "<path>", line 122, in <module>
    print_five_items([5, 2])
  File "<path>", line 117, in print_five_items
    raise ValueError("L'argomento deve avere cinque elementi")
ValueError: L'argomento deve avere cinque elementi

Nota come l'ultima riga mostra il messaggio descrittivo:

ValueError: L'argomento deve avere cinque elementi

Puoi quindi scegliere come gestire l'eccezione con una istruzione try ... except. Potresti aggiungere una clausola else e/o una clausola finally. Puoi gestirla secondo le tue necessità.

🔹 Risorse Utili (in lingua inglese)

Spero che ti sia piaciuto leggere il mio articolo e che tu l'abbia trovato utile. Ora hai gli strumenti necessari per gestire le eccezioni in Python e puoi usarli a tuo vantaggio quando scrivi il codice Python. Dai un'occhiata ai miei corsi online. Puoi seguirmi su Twitter.

⭐️ Ti potrebbero piacere anche altri miei articoli su freeCodeCamp: