Gestione degli errori e testing

Introduzione

In questa guida tratteremo come deve fare il nostro programma quando incontra situazioni impreviste e vedremo come testare il codice che scriviamo. In particolare, descriveremo il formato degli esercizi proposti nella parte sui Fondamenti Python.

Che fare

  • scompatta lo zip in una cartella, dovresti ottenere qualcosa del genere:

errors-and-testing
    errors-and-testing.ipynb
    errors-and-testing-sol.ipynb
    jupman.py

ATTENZIONE: Per essere visualizzato correttamente, il file del notebook DEVE essere nella cartella szippata.

  • apri il Jupyter Notebook da quella cartella. Due cose dovrebbero aprirsi, prima una console e poi un browser. Il browser dovrebbe mostrare una lista di file: naviga la lista e apri il notebook errors-and-testing.ipynb

  • Prosegui leggendo il file degli esercizi, ogni tanto al suo interno troverai delle scritte ESERCIZIO, che ti chiederanno di scrivere dei comandi Python nelle celle successive. Gli esercizi sono graduati per difficoltà, da una stellina ✪ a quattro ✪✪✪✪

Scorciatoie da tastiera:

  • Per eseguire il codice Python dentro una cella di Jupyter, premi Control+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E selezionare la cella seguente, premi Shift+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E creare una nuova cella subito dopo, premi Alt+Invio

  • Se per caso il Notebook sembra inchiodato, prova a selezionare Kernel -> Restart

Situazioni inaspettate

E’ sera, c’è da festeggiare un compleanno e perciò ti è stato chiesto di fare una torta. Sai che servono i seguenti passi:

  1. prendi il latte

  2. prendi lo zucchero

  3. prendi la farina

  4. impasta

  5. scalda nel forno

Prendi il latte, lo zucchero, ma scopri di non avere la farina. E’ sera, e non ci sono negozi aperti. Evidentemente, non ha senso proseguire al punto 4 con l’impasto e devi rinunciare a fare la torta, segnalando al festeggiato/a il problema. Puoi solo sperate che il festeggiato/a decida per qualche alternativa di suo gradimento.

Traducendo il tutto in termini di Python, possiamo chiederci se durante l’esecuzione di una funzione, di fronte ad una situazione imprevista è possibile :

  1. interrompere il flusso di esecuzione del programma

  2. segnalare a chi ha chiamato la funzione che c’è stato un problema

  3. permettere di gestire a chi ha chiamato la funzione il problema

La risposta è sì e si fa col meccanismo delle eccezioni (Exception)

fai_torta_problematica

Vediamo intanto come possiamo rappresentare in Python il problema di sopra. Una versione di base potrebbe essere essere la seguente:

DOMANDA: questa versione ha un problema serio. Riesci a vederlo ??

[1]:
def fai_torta_problematica(latte, zucchero, farina):
    """ - supponiamo servano 1.3 kg per il latte, 0.2kg per lo zucchero e 1.0kg per la farina

        - prende come parametri le quantità che abbiamo in dispensa
    """

    if latte > 1.3:
        print("prendo il latte")
    else:
        print("Non ho abbastanza latte !")

    if zucchero > 0.2:
        print("prendo lo zucchero")
    else:
        print("Non ho abbastanza zucchero !")

    if farina > 1.0:
        print("prendo la farina")
    else:
        print("Non ho abbastanza farina !")

    print("Impasto")
    print("Scaldo")
    print("Ho fatto la torta !")


fai_torta_problematica(5,1,0.3)  # poca farina ...

print("Festeggia")
prendo il latte
prendo lo zucchero
Non ho abbastanza farina !
Impasto
Scaldo
Ho fatto la torta !
Festeggia
Mostra risposta

Controllare con i return

ESERCIZIO: Potremmo correggere i problemi della torta di prima aggiungendo dei comandi return. Implementa la seguente funzione.

NOTA: NON spostare il print("Festeggia") dentro la funzione. Lo scopo dell’esercizio è proprio tenerlo fuori così da usare il valore ritornato da fai_torta per decidere se festeggiare o meno.

Se hai dubbi sulle funzioni con valori di ritorno, puoi consultare il Capitolo 6 di Pensare in Python

Mostra soluzione
[2]:
def fai_torta(latte, zucchero, farina):
    """  - supponiamo servano 1.3 kg per il latte, 0.2kg per lo zucchero e 1.0kg per la farina

         - prende come parametri le quantità che abbiamo in dispensa
         MIGLIORARE CON LE return: RITORNA True se la torta è fattibile,
                                           False altrimenti
         *FUORI* USA IL VALORE RITORNATO PER FESTEGGIARE O MENO

    """
    # implementa qui la funzione



# e scrivi qui la chiamata a funzione, fai_torta(5,1,0.3)
# usando il risultato per dichiarare se si può o meno fare il party :-(


prendo il latte
prendo lo zucchero
Non ho abbastanza farina !
No party !

Le eccezioni

Riferimenti: Nicola Cassetta - 19: La gestione delle eccezioni

Usando i return abbiamo migliorato la funzione precedente, ma rimane un problema: la responsabilità di capire se la torta è riuscita rimane al chiamante della funzione, che deve prendere il valore ritornato e decidere in base a quello se festeggiare o meno. Un programmatore sbadato potrebbe dimenticarsi di effettuare il controllo e feteggiare anche con una torta mal riuscita.

Quindi ci chiediamo: è possibile fermare l’esecuzione non solo della funzione ma di tutto il programma non appena riscontriamo una situazione imprevista?

Per migliorare quanto sopra, si possono usare le eccezioni. Per dire a Python di interrompere l’esecuzione del programma in un punto, si può inserire l’istruzione raise così:

raise Exception()

Volendo, si può anche scrivere un messaggio che aiuti i programmatori (potreste essere voi stessi …) a capire l’origine del problema. Nel nostro caso potrebbe essere un messaggio di questo tipo:

raise Exception("Non c'è abbastanza farina !")

Nota: nei programmi professionali, i messaggi delle exception sono intesi per programmatori, sono verbosi, e tipicamente finiscono nascosti nei log di sistema. Agli utenti finali si dovrebbero mostrare messaggi più brevi e comprensibili da un pubblico non tecnico. Al più, si può includere un codice di errore che l’utente può copiare e dare ai tecnici per aiutare a trovare il problema.

ESERCIZIO: Prova a riscrivere la funzione qua sopra sostituendo le righe con return con dei raise Exception() :

Mostra soluzione
[3]:
def fai_torta_eccezionale(latte, zucchero, farina):
    """ - supponiamo servano 1.3 kg per il latte, 0.2kg per lo zucchero e 1.0kg per la farina

        - prende come parametri le quantità che abbiamo in dispensa
        - se mancano ingredienti, lancia Exception

    """
    # implementa la funzione

Una volta implmenetata, scrivendo

fai_torta_eccezionale(5,1,0.3)
print("Festeggia")

Dovresti vedere (nota come “Festeggia” non venga mai stampato):

prendo il latte
prendo lo zucchero

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-10-02c123f44f31> in <module>()
----> 1 fai_torta_eccezionale(5,1,0.3)
      2
      3 print("Festeggia")

<ipython-input-9-030239f08ca5> in fai_torta_eccezionale(latte, zucchero, farina)
     18         print("prendo la farina")
     19     else:
---> 20         raise Exception("Non ho abbastanza farina !")
     21     print("Impasto")
     22     print("Scaldo")

Exception: Non ho abbastanza farina !

Vediamo che il programma si è interrotto prima di passare all’impasto (dentro la funzione), e non è neppure stato fatto il festaggiamento (che è fuori dalla funzione). Proviamo adesso a chiamare la funzione con abbastanza elementi in dispensa:

[4]:
fai_torta_eccezionale(5,1,20)
print("Festeggia")
prendo il latte
prendo lo zucchero
prendo la farina
Impasto
Scaldo
Ho fatto la torta !
Festeggia

Gestire le eccezioni

E se invece di interrompere brutalmente il programma in caso di problemi, volessimo provare qualche alternativa (come andare a comprare del gelato …) ? Potremmo usare i blocchi try except così:

[5]:
try:
    fai_torta_eccezionale(5,1,0.3)
    print("Festeggia")
except:
    print("Non riesco a fare la torta, che ne dite se usciamo a prendere del gelato?")
prendo il latte
prendo lo zucchero
Non riesco a fare la torta, che ne dite se usciamo a prendere del gelato?

Se noti, l’esecuzione ha saltato il print("Festeggia") ma non è stata stampata nessuna eccezione, e l’esecuzione è passata alla riga subito dopo l’except

Eccezioni particolari

Finora abbiamo usato Exception che è un’eccezione generica, ma volendo si può usare eccezioni più specifiche per segnalare meglio il tipo di errore che è accaduto. Per esempio, quando si implementa una funzione, dato che il controllo del valore corretto dei parametri come in fai_torta_eccezionale è qualcosa che si fa molto spesso, Python mette a disposizione una eccezione di nome ValueError. Usandola invece di Exception, si consente a chi chiama la funzione di intercettare solo quella tipologia di errore.

Se invece nella funzione viene lanciato un errore che nel catch non viene intercettato, il programma si interromperà.

[6]:

def fai_torta_eccezionale_2(latte, zucchero, farina):
    """ - supponiamo servano 1.3 kg per il latte, 0.2kg per lo zucchero e 1.0kg per la farina

        - prende come parametri le quantità che abbiamo in dispensa
        - se mancano ingredienti, lancia ValueError
    """

    if latte > 1.3:
        print("prendo il latte")
    else:
        raise ValueError("Non ho abbastanza latte !")
    if zucchero > 0.2:
        print("prendo lo zucchero")
    else:
        raise ValueError("Non ho abbastanza zucchero!")
    if farina > 1.0:
        print("prendo la farina")
    else:
        raise ValueError("Non ho abbastanza farina !")
    print("Impasto")
    print("Scaldo")
    print("Ho fatto la torta !")

try:
    fai_torta_eccezionale_2(5,1,0.3)
    print("Festeggia")
except ValueError:
    print()
    print("Ci deve essere un problema con gli ingredienti!")
    print("Proviamo a chiedere ai vicini !")
    print("Che fortuna, ci hanno dato della farina, riproviamo !")
    print("")
    fai_torta_eccezionale_2(5,1,4)
    print("Festeggia")
except:  # gestisce tutte le altre eccezioni
    print("Ragazzi, è successo un problema grave, no so come fare. Meglio uscire a prendere il gelato!")

prendo il latte
prendo lo zucchero

Ci deve essere un problema con gli ingredienti!
Proviamo a chiedere ai vicini !
Che fortuna, ci hanno dato della farina, riproviamo !

prendo il latte
prendo lo zucchero
prendo la farina
Impasto
Scaldo
Ho fatto la torta !
Festeggia

Per ulteriori spiegazioni su i try catch rimandiamo alla lezioni di Nicola Cassetta - 19: La gestione delle eccezioni

Gli assert

Ti è stato chiesto di scrivere un programma per controllare un reattore nucleare. Il reattore produce tanta energia, ma ha anche bisogno di almeno 20 metri d’acqua per raffreddarsi, e il tuo programma deve regolare il livello dell’acqua. Senza acqua sufficiente, rischi una fusione. Non ti senti esattamente all’altezza del compito, e cominci a sudare

Con un certo nervosismo scrivi il codice. Controlli e ricontrolli il programma, credi che sia tutto giusto.

Il giorno dell’inaugurazione, il reattore viene fatto partire. Inaspettatamente, il livello dell’acqua scende a 5 metri, e inizia la reazione. Seguono spettacoli pirotecnici al plutonio.

Avremmo potuto evitare tutto ciò? A volte crediamo sia tutto giusto ma per qualche motivo ci ritroviamo variabili con valori inattesi. Il programma sbagliato descritto qua sopra, in Python avrebbe potuto essere così:

[7]:
# ci serve acqua per raffreddare il nostro reattore nucleare
livello_acqua = 40 #  sembra sufficiente

print("livello_acqua: ", livello_acqua)

# tanto codice

# tanto codice

# tanto codice

# tanto codice

livello_acqua = 5  # abbiamo dimenticato questa riga nefasta !
print("ATTENZIONE: livello acqua basso! ", livello_acqua)

# tanto codice

# tanto codice

# tanto codice

# tanto codice

# dopo tanto codice potremmo non sapere se ci sono le condizioni necessarie
# affinchè tutto funzioni correttamente

print("fai partire reattore nucleare")


livello_acqua:  40
ATTENZIONE: livello acqua basso!  5
fai partire reattore nucleare

Come potremmo migliorarlo? Guardiamo come funziona il comando assert, che va scritto facendolo seguire da una condizione booleana.

assert True non fa assolutamente niente:

[8]:
print("prima")
assert True
print("dopo")
prima
dopo

Invece, assert False blocca l’esecuzione del programma, lanciando un’eccezione di tipo AssertionError (nota come "dopo" non venga stampato):

print("prima")
assert False
print("dopo")
prima
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-7-a871fdc9ebee> in <module>()
----> 1 assert False

AssertionError:

Per migliorare il programma precedente, potremmo usare assert così:

# ci serve acqua per raffreddare il nostro reattore nucleare
livello_acqua = 40 #  livello sufficiente

print("livello_acqua: ", livello_acqua)

# tanto codice

# tanto codice

# tanto codice

# tanto codice

livello_acqua = 5  # abbiamo dimenticato questa riga nefasta !
print("ATTENZIONE: livello acqua basso! ", livello_acqua)

# tanto codice

# tanto codice

# tanto codice

# tanto codice


# dopo tanto codice potremmo non sapere se ci sono le condizioni necessarie
# affinchè tutto funzioni correttamente
# quindi prima di fare cose pericolose, è sempre meglio fare un controllo !
# se l'assert fallisce (cioè se l'espressione booleana è False)
# l'esecuzione si blocca subito
assert livello_acqua >= 20

print("fai partire reattore nucleare")
livello_acqua:  40
ATTENZIONE: livello acqua basso!  5

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-14-9019745468f3> in <module>
     29 # se l'assert fallisce (cioè se l'espressione booleana è False)
     30 # l'esecuzione si blocca subito
---> 31 assert livello_acqua >= 20
     32
     33 print("fai partire reattore nucleare")

AssertionError:

Quando usare gli assert?

Il caso qua sopra è volutamente esagerato, ma mostra come un controllo in più a volte impedisca disastri.

NOTA: Gli assert sono un modo molto spiccio di fare controlli, tanto che Python permette anche anche di ignorarli durante l’esecuzione per migliorare le performance, chiamando python con il parametro -O come in:

python -O mio_file.py

Ma se le performance non sono un problema (nel caso del reattore qui sopra), conviene riscrivere il programma usando un if e lanciando esplicitamente una Exception:

# ci serve acqua per raffreddare il nostro reattore nucleare
livello_acqua = 40 #  seems ok

print("livello_acqua: ", livello_acqua)

# tanto codice

# tanto codice

# tanto codice

# tanto codice

livello_acqua = 5  # abbiamo dimenticato questa riga nefasta !
print("ATTENZIONE: livello acqua basso! ", livello_acqua)

# tanto codice

# tanto codice

# tanto codice

# tanto codice


# dopo tanto codice potremmo non sapere se ci sono le condizioni necessarie
# affinchè tutto funzioni correttamente
# quindi prima di fare cose pericolose, è sempre meglio fare un controllo !
if livello_acqua < 20:
    raise Exception("Livello acqua troppo basso !")  # l'esecuzione si blocca subito

print("fai partire reattore nucleare")
livello_acqua:  40
ATTENZIONE: livello acqua basso!  5

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-16-02382ff90f5a> in <module>
     28 # quindi prima di fare cose pericolose, è sempre meglio fare un controllo !
     29 if livello_acqua < 20:
---> 30     raise Exception("Livello acqua troppo basso !")  # l'esecuzione si blocca subito
     31
     32 print("fai partire reattore nucleare")

Exception: Livello acqua troppo basso !

Usare gli assert per testare

Nella parte sui Fondamenti, usiamo spesso gli assert per fare dei test, cioè per verificare che una funzione si comporti come ci si attende.

Per esempio, per questa funzione:

[9]:
def somma(x, y):
    s = x + y
    return s

Ci si attende che somma(2,3) dia 5. Possiamo scrivere in Python questa attesa usando un assert:

[10]:
assert somma(2,3) == 5

Se somma è implementata correttamente:

  1. somma(2,3) ci dara 5

  2. l’espression booleana somma(2,3) == 5 darà True

  3. assert True verrà eseguita senza produrre alcun risultato, lasciando proseguire l’esecuzione del programma

Viceversa, se somma NON è implementata correttamente come in questo caso:

def somma(x,y):
    return 666
  1. somma(2,3) produrrà il numero 666

  2. l’espression booleana somma(2,3) == 5 darà quindi False

  3. assert False interromperà l’esecuzione del programma, lanciando un eccezione di tipo AssertionError

Struttura esercizi parte Fondamenti

Ricapitolando, gli esercizi della parte Fondamenti Python nelle sezioni Verifica Comprensione sono spesso strutturati nel seguente formato:

def somma(x,y):
    """ RITORNA la somma dei numeri x e y
    """
    raise Exception("IMPLEMENTAMI")


assert somma(2,3) == 5
assert somma(3,1) == 4
assert somma(-2,5) == 3

Se tenti di eseguire la cella, vedrai questo errore:

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-16-5f5c8512d42a> in <module>()
      6
      7
----> 8 assert somma(2,3) == 5
      9 assert somma(3,1) == 4
     10 assert somma(-2,5) == 3

<ipython-input-16-5f5c8512d42a> in somma(x, y)
      3     """ RITORNA la somma dei numeri x e y
      4     """
----> 5     raise Exception("IMPLEMENTAMI")
      6
      7

Exception: IMPLEMENTAMI

Per risolverli, dovrai:

  1. sostituire la riga raise Exception("IMPLEMENTAMI") con il corpo della funzione

  2. eseguire la cella

Se l’esecuzione della cella non risulta in eccezioni lanciate, perfetto ! Vuol dire che la funzione fa quello che ci si attende (gli assert quando vanno a buon fine non producono output)

Se invece vedi qualche AssertionError, probabilmente hai sbagliato qualcosa.

NOTA: Il raise Exception("IMPLEMENTAMI") è messo per ricordarti che la funzione ha un problema grosso, e cioè che non ha il codice !!
In programma lunghi, può capitare di sapere che c’è bisogno di una funzione, ma di non sapere al momento quale codice mettere nel corpo. Invece di scrivere nel corpo dei comandi che non fanno niente tipo print() o return None, è MOLTO meglio lanciare delle eccezioni così che se per caso si esegue il programma per sbaglio, appena Python raggiunge la funzione non implementata, l’esecuzione viene subito fermata segnalando all’utente la natura e posizione del problema. Infatti, diversi editor per programmatori quando generano automaticamente codice mettono negli scheletri di funzione da implementare delle Exception di questo tipo.

Proviamo a scrivere volutamente un corpo di funzione sbagliato, che ritorna sempre 5 indipendentemente dall’x e y di ingresso:

def somma(x,y):
    """ RITORNA la somma dei numeri x e y
    """
    return 5

assert somma(2,3) == 5
assert somma(3,1) == 4
assert somma(-2,5) == 3

In questo caso la prima asserzione ha successo e quindi semplicemente passa l’esecuzione passa alla riga successiva che contiene un’altro assert. Ci si attende che somma(3, 1) dia 4, ma la nostra funzione implementata male ritorna 5 e quindi questo assert fallisce. Notare come l’esecuzione si interrompa al secondo assert:

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-19-e5091c194d3c> in <module>()
      6
      7 assert somma(2,3) == 5
----> 8 assert somma(3,1) == 4
      9 assert somma(-2,5) == 3

AssertionError:

Implementando la funzione bene ed eseguendo la cella non vedremo nessun output: significa che la funzione ha passato i test e possiamo concludere che è corretta rispetto ai test.

ATTENZIONE: ricordati sempre che questo tipo di test non sono quasi mai esaustivi! Il fatto che i test passino è solo un’indicazione che la funzione potrebbe essere corretta, ma non una certezza !

[11]:
def somma(x,y):
    """ RITORNA la somma dei numeri x e y
    """
    return x + y

# Adesso la funzione è corretta, eseguendo questa cella non dovresti vedere alcun output

assert somma(2,3) == 5
assert somma(3,1) == 4
assert somma(-2,5) == 3

ESERCIZIO: Prova a scrivere il corpo della funzione moltiplica:

  • sostituisci raise Exception("IMPLEMENTAMI") con return x * y ed eseguire la cella. Se hai scritto giusto, non dovrebbe succedere niente. In tal caso complimenti, il codice che hai scritto è corretto rispetto ai test!

  • Prova poi a sostituire invece con return 10 e guarda che succede.

Mostra soluzione
[12]:
def moltiplica(x,y):
    """ RITORNA la somma dei numeri x e y
    """
    raise Exception('TODO IMPLEMENT ME !')

assert moltiplica(2,5) == 10
assert moltiplica(0,2) == 0
assert moltiplica(3,2) == 6