19: LA GESTIONE DELLE ECCEZIONI

ANCORA SUGLI ERRORI DI RUNTIME

Ormai sappiamo bene cos'è un errore di runtime: se durante l'esecuzione di un programma chiediamo a Python di fare qualcosa di illegale (ad es. dividere per zero, usare un indice inesistente in una lista o chiamare una funzione con un numero errato di argomenti) Python blocca immediatamente l'esecuzione, esce dal programma e ci segnala l'errore. Questo comportamento va benissimo durante lo sviluppo del programma, ma sarebbe piuttosto imbarazzante in un programma "serio". E' facilissimo provocare un errore di runtime con un input sbagliato: ecco un esempio:

NUOVO PROGRAMMA: prova_eccezioni.py

nomi = ["Gino", "Peppe", "Carlo", "Ciccio"]
n = int(input("Ho pensato quattro nomi. Quale vuoi sapere? "))
n -= 1    # ricorda che gli indici vanno da 0 a 3
print(nomi[n])

Quando lanciamo questo programma possiamo allegramente provocare un ValueError digitando "xyz" al prompt (Python deve tradurre la nostra stringa in un intero, ma la stringa non ha significato). Oppure possiamo provocare un IndexError digitando "10" (nell'istruzione successiva chiediamo a Python di leggere il decimo elemento di una lista con solo quattro elementi).

Il secondo errore potrebbe essere evitato usando la funzione input_int() che abbiamo programmato nella Lezione 17, ma per il primo non sembra esserci soluzione: se un programma commerciale andasse in crash per un problema così stupido probabilmente nessuno sarebbe disposto a spendere soldi per acquistarlo!

Naturalmente la soluzione c'è: la terminazione del programma è solo il comportamento predefinito, ma Python (come tutti i linguaggi più evoluti) permette di gestire gli errori di runtime, facendo in modo che, quando accadono, vengano eseguite delle istruzioni definite dal programmatore (che di solito servono a correggere l'errore) ed il programma possa continuare.

LE ISTRUZIONI try ... except

Nel linguaggio informatico il termine "eccezione" (Inglese: exception) ha via via sostituito il termine "errore" (error), sicchè oggi si usano entrambi come sinonimi. Modifichiamo il programma precedente in questo modo:


nomi = ["Gino", "Peppe", "Carlo", "Ciccio"]
try:
    n = int(input("Ho pensato quattro nomi. Quale vuoi sapere? "))
    n -= 1    # ricorda che gli indici vanno da 0 a 3
    print(nomi[n])
except:
    print("Input non valido!")

Lanciatelo e provate a scrivere al prompt qualcosa che non ha significato: il programma non vi darà più un errore, ma scriverà "Input non valido". La coppia di istruzioni try ... except si usa per gestire le eccezioni. Esse devono essere allo stesso livello di indentazione e devono essere entrambe seguite da un blocco di codice (indentato). Python tenta dapprima di eseguire il blocco try; se in esso si verifica un errore, salta immediatamente (senza terminarlo) al blocco except, che esegue per intero. Se invece non si verificano errori l'except viene saltato. Notiamo che try ed except, come tutte le istruzioni che modificano il flusso del programma, sono seguite dai due punti.

In ogni caso, dopo aver eseguito il try o l'except il programma continua con la prima istruzione successiva allo stesso livello di indentazione, e così possiamo evitare che un errore mandi in crash il programma.

Quindi nel nostro esempio Python inizia eseguendo il blocco try; se nella input() si verifica un errore salta le altre due righe del try ed esegue immediatamente il blocco except. Se invece immettiamo un input corretto Python esegue tutto il try e salta l'except (e quindi non scrive "Input non valido").

Vediamo ora come possiamo fare per far ripetere l'input fino a quando otteniamo il risultato voluto. Modificate così:


nomi = ["Gino", "Peppe", "Carlo", "Ciccio"]
while True:
    try:
        n = int(input("Ho pensato quattro nomi. Quale vuoi sapere? "))
        n -= 1    # ricorda che gli indici vanno da 0 a 3
        print(nomi[n])
        break
    except:
        print("Input non valido!")

I due blocchi try ... except sono ora inseriti all'interno di un ciclo while infinito: se nel try si verifica un errore il programma salterà all'except, avvisando dell'input scorretto e ripetendo il while; se invece tutto va bene viene eseguito il break alla fine del try e si esce dal ciclo continuando il programma.

ALCUNE SINTASSI PIU' COMPLICATE

L'istruzione except può essere seguita dal nome di una o più eccezioni (ad es. NameError, TyperError, ValueError ...) separati da virgole (attenti alla corrispondenza tra maiuscole e minuscole). In questo caso essa intercetta solo quel tipo di eccezioni, mentre le altre non vengono gestite. Dopo una try ci possono essere più except in modo tale da gestire diversamente errori diversi.


nomi = ["Gino", "Peppe", "Carlo", "Ciccio"]
while True:
    try:
        n = int(input("Ho pensato quattro nomi. Quale vuoi sapere? "))
        n -= 1    # ricorda che gli indici vanno da 0 a 3
        print(nomi[n])
        break
    except ValueError:       # viene eseguita se abbiamo introdotto una stringa senza senso
        print("Devi introdurre un numero!")
    except IndexError:       # viene eseguita se abbiamo introdotto un numero sbagliato
        print("Hai introdotto un numero non valido!")

Va detto che la try ... except non è un modo "miracoloso" per mascherare i nostri errori di programmazione, ma dovrebbe essere usata solo per gestire delle eccezioni che il programmatore ha previsto. Per questo i programmatori esperti considerano pericoloso l'uso di except da solo, in quanto potrebbe "catturare" anche eccezioni che derivano da nostri errori di programmazione. A volte si mette un except come ultima istruzione, ma questo va considerato una sorta di "ultima spiaggia" per non mandare in crash il programma:


nomi = ["Gino", "Peppe", "Carlo", "Ciccio"]
while True:
    try:
        n = int(input("Ho pensato quattro nomi. Quale vuoi sapere? "))
        n -= 1    # ricorda che gli indici vanno da 0 a 3
        print(nomi[n])
        break
    except ValueError:
        print("Devi introdurre un numero!")
    except IndexError:
        print("Hai introdotto un numero non valido!")
    except:
        print("Oops! Errore imprevisto! Non so cosa e' successo")

Probabilmente l'ultimo except non verrà mai eseguito: nel caso lo fosse dovremmo probabilmente eseguire il programma con il debugger per capire quale tipo di errore sia capitato (e gestirlo di conseguenza).

Inoltre anche la try può essere seguita da una else (che, se presente, va in coda a tutte le except), che viene eseguita solo se nel blocco try non si è verificato alcun errore. La prassi comune è di limitare al minimo le istruzioni nel try (possibilmente solo quelle che potenzialmente potrebbero dare un errore che abbiamo previsto) ed aggiungere le altre istruzioni nell'else: anche questo in modo da limitare al massimo la possibilità che nel try si verifichi un'eccezione non prevista, "mascherando" un nostro errore di programmazione. Ecco infine il nostro programma aggiornato:


nomi = ["Gino", "Peppe", "Carlo", "Ciccio"]
while True:
    try:
        n = int(input("Ho pensato quattro nomi. Quale vuoi sapere? "))
        n -= 1    # ricorda che gli indici vanno da 0 a 3
        print(nomi[n])
    except ValueError:
        print("Devi introdurre un numero!")
    except IndexError:
        print("Hai introdotto un numero non valido!")
    except:
        print("Oops! Errore imprevisto! Non so cosa e' successo")
    else:
        break

Nel try sono state lasciate solo le due istruzioni potenzialmente pericolose (la input() che potrebbe provocare un ValueError e la print(nomi[n]) che potrebbe dare un IndexError), mentre il break è stato spostato nell'else.

LE ECCEZIONI E LE FUNZIONI

Il blocco try ... except gestisce anche le eccezioni che si verificano all'interno delle funzioni che vengono chiamate nel try:


from math import *

try:
    n = float(input("Scrivi un numero "))
    print(sqrt(n))
    print(log(n))
except ValueError:
    print("Input scorretto")

Se immettiamo 0 o un numero negativo le funzioni sqrt() o log() provocheranno un errore, che sarà intercettato dal nostro except. Quando usiamo le funzioni dobbiamo perciò sapere quali possibili eccezioni esse possono provocare, e, se non vogliamo che esse interrompano l'esecuzione del programma, dobbiamo preoccuparci di gestirle.

L'ISTRUZIONE pass

Questa istruzione ... non fa nulla! E' però talvolta necessaria in alcune situazioni come questa:


while True:
    try:
        n = float(input("Scrivi un numero "))
    except ValueError:
        pass
    else:
        break

In questo caso, se si verifica un'eccezione, vogliamo solo ripetere il prompt "Scrivi un numero ". Non possiamo scrivere solo except, in quanto essa deve essere obbligatoriamente seguita da un blocco di codice indentato, così diventa necessaria un'istruzione "che non fa nulla".

Il pass si può mettere ovunque, la sua funzione è principalmente quella di prendere il posto di un blocco indentato. A volte viene usato provvisoriamente in un elif, come segnaposto per dei casi che non abbiamo ancora programmato.


n = int(input("Scrivi un numero "))
if(n == 1):
    print("Questo e' il caso 1")
    fai_calcoli_complicati_con_1()
elif(n == 2):
    pass    # questo caso lo scrivo dopo, ma per ora e' necessario un blocco indentato
elif(n == 3):
    pass    # idem

L'ISTRUZIONE raise

Possiamo infine forzare volontariamente un'eccezione con l'istruzione raise. La sintassi è

raise nome_eccezione[(parametro)]

il parametro è opzionale e, se presente, è di solito una stringa che viene scritta subito dopo il nome dell'errore come spiegazione. Ad esempio provate a scrivere in IDLE:


>>> raise NameError

Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    raise NameError
NameError

>>> raise NameError("Nome sbagliato")

Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    raise NameError("Nome sbagliato")
NameError: Nome sbagliato

Il raise è usato spesso all'interno delle funzioni da noi programmate, per segnalare al programma principale che qualcosa è andato storto: di solito se succede qualcosa di anomalo all'interno di una funzione il programmatore scrive il raise senza nessun blocco try, così Python genera un errore ed esce immediatamente dalla funzione. Ad esempio riprendiamo il programma radice.py che avevamo implementato nella Lezione 15.

ESERCIZIO 19.1 Trasformate il programma in una funzione mysqrt()che prenda come argomento un numero e restituisca la sua radice, provocando un ValueError se il suo argomento è negativo. Dovete eliminare, nella funzione, tutte le istruzioni che stampano i risultati intermedi e trasformare l'ultima istruzione (che stampa la radice in una return. Scrivete poi due istruzioni che chiamino tale funzione, una con un numero positivo ed una con un numero negativo come argomento.
SOLUZIONI

Avevamo già notato che il nostro programma non funziona se chiediamo la radice di un numero negativo (che del resto non esiste) ed avevamo risolto il problema fermando il calcolo e stampando un messaggio. Questo può andare bene nel programma principale, ma non è auspicabile in una funzione: il programma chiamante si aspetta infatti che la funzione gli restituisca un valore, e se questo non avvenisse potrebbero verificarsi errori imprevisti. Molto meglio sollevare un'eccezione:


def mysqrt(n):
    if n < 0:
        raise ValueError("mysqrt e' stata chiamata con un argomento negativo")
    . . .

In questo modo, se chiamate la funzione con un numero negativo come argomento, la funzione terminerà immediatamente e segnalerà l'errore al programma principale, che potrà eventualmente gestirlo

AGGIORNIAMO myinput.py

Riprendiamo il nostro modulo myinput.py ed introduciamo la gestione delle eccezioni. Le funzioni "problematiche" sono quelle che convertono una stringa in intero: abbiamo visto sopra che se la stringa non ha significato si ottiene un ValueError. Vediamo ad esempio la funzione input_int()


def input_int(prompt, min, max):
    while True:
        risp = int(input(prompt))
        if risp >= min and risp <= max:
            return risp
        print("Input scorretto")

Modifichiamola così:


def input_int(prompt, min, max):
    while True:
        try:
            risp = int(input(prompt))
            if risp >= min and risp <= max:
                return risp
        except ValueError:
            print ("Input scorretto")
        print("Input scorretto")

Ora la funzione fa quello che deve fare, ma per un informatico è "esteticamente brutto" ripetere due volte la print() (la ripetizione è necessaria: una riga viene eseguita in caso di eccezione, l'altra se il numero immesso non è nei limiti giusti). Ecco un trucco per rimediare:


def input_int(prompt, min, max):
    while True:
        try:
            risp = int(input(prompt))
            if risp < min or risp > max:
                raise ValueError
            return risp
        except ValueError:
            print ("Input scorretto")

In questo modo, se l'utente ha immesso un numero non nei limiti giusti, generiamo un errore e provochiamo l'esecuzione della except.

ESERCIZIO 19.2 Modificare nello stesso modo anche la funzione input_menu()
SOLUZIONI

Facciamo esplicitamente notare che dopo il miglioramento delle funzioni tutti i programmi che usano questo modulo saranno automaticamente aggiornati ogni volta che li lanceremo. Se avessimo scritto le funzioni direttamente nei programmi ora dovremmo cercare tutti i file in cui abbiamo utilizzato queste funzioni e modificarli. Questo è un altro motivo a favore del raccoglimento delle funzioni in moduli separati.

Fine della lezione