Previous Up Next

Buy this book at Amazon.com

Appendix A  Debug

Nell’operazione di rimozione degli errori da un programma, è opportuno distinguere i vari tipi di errore in modo da poterli rintracciare più velocemente:

  • Gli errori di sintassi vengono scoperti dall’interprete quando traduce il codice sorgente in codice macchina. Indicano che c’è qualcosa di sbagliato nella struttura del programma. Esempio: omettere i due punti alla fine di una istruzione def genera il messaggio, un po’ ridondante, SyntaxError: invalid syntax.
  • Gli errori di runtime sono prodotti dall’interprete se qualcosa va storto durante l’esecuzione del programma. Nella maggior parte dei casi, il messaggio di errore specifica dove si è verificato l’errore e quali funzioni erano in esecuzione. Esempio: una ricorsione infinita causa un errore di runtime “maximum recursion depth exceeded”.
  • Gli errori di semantica sono dei problemi con un programma che viene eseguito senza produrre messaggi di errore, ma che non fa le cose nel modo giusto. Esempio: un’espressione può essere valutata secondo un ordine diverso da quello che si intendeva, generando un risultato non corretto.

La prima cosa da fare nel debug è capire con che tipo di errore abbiamo a che fare. Anche se i paragrafi che seguono sono organizzati per tipo di errore, alcune tecniche sono applicabili in più di una situazione.

A.1  Errori di sintassi

Gli errori di sintassi sono in genere facili da sistemare, una volta capito in cosa consistono. Purtroppo, il messaggio di errore spesso è poco utile. Quelli più comuni sono: SyntaxError: invalid syntax e SyntaxError: invalid token, nessuno dei quali è molto esplicativo.

In compenso, il messaggio comunica il punto del programma dove si è verificato il problema. Più precisamente, dice dove Python ha notato il problema, che non è necessariamente il punto in cui si trova l’errore. A volte l’errore è prima del punto indicato dal messaggio, spesso nella riga precedente.

Se state costruendo il programma in modo incrementale, è molto probabile che l’errore sia nell’ultima riga che avete aggiunto.

Se state copiando il codice da un libro, cominciate confrontando attentamente il vostro codice con quello del libro. Controllate ogni carattere. Ricordate però che anche il libro può essere sbagliato, e se vedete qualcosa che somiglia a un errore di sintassi, potrebbe esserlo.

Ecco alcuni modi di evitare i più comuni errori di sintassi:

  1. Accertatevi di non usare parole chiave di Python come nomi di variabile.
  2. Controllate che ci siano i due punti alla fine di ogni intestazione di tutte le istruzioni composte, inclusi for, while, if, e le istruzioni def.
  3. Accertatevi che ogni stringa nel codice sia racchiusa da una coppia di virgolette o apici, e che questi siano di tipo indifferenziato e non “tipografici”.
  4. Se avete stringhe a righe multiple con triple virgolette, accertatevi di averle chiuse in modo appropriato. Una stringa non chiusa può causare un errore di invalid token al termine del programma, oppure può trattare la parte che segue del programma come fosse una stringa, finché non incontra la stringa successiva. Nel secondo caso, potrebbe anche non produrre affatto un messaggio di errore!
  5. Un operatore di apertura non chiuso—(, {, o [—fa sì che Python consideri la riga successiva come parte dell’istruzione corrente. In genere, si verifica un errore nella riga immediatamente successiva.
  6. Controllate che non ci sia il classico = al posto di == all’interno di un’istruzione condizionale.
  7. Controllate l’indentazione per assicurarvi che il codice sia allineato nel modo corretto. Python può gestire sia spazi che tabulazioni, ma se li mescolate possono esserci problemi. Il modo migliore di evitarli è usare un editor di testo appositamente realizzato per Python, che gestisca l’indentazione in modo coerente.
  8. Se nel codice (compresi stringhe e commenti) ci sono caratteri non-ASCII, tipo le lettere accentate, potrebbero causare problemi, anche se Python 3 gestisce abbastanza bene questi caratteri. Fate attenzione, se copiate e incollate del testo da una pagina web o da altre fonti.

Se nulla di tutto questo funziona, continuate con il paragrafo seguente...

A.1.1  Continuo a fare modifiche ma non cambia nulla.

Se l’interprete dice che c’è un errore ma non lo trovate, può essere che voi e l’interprete non stiate guardando lo stesso codice. Controllate il vostro ambiente di programmazione per assicurarvi che il programma che state modificando sia proprio quello che Python sta tentando di eseguire.

Se non ne siete certi, provate a mettere deliberatamente un evidente errore di sintassi all’inizio del programma e rieseguitelo. Se l’interprete non trova l’errore, non state eseguendo il nuovo codice.

Alcune cause possibili:

  • Avete modificato il file e dimenticato di salvare le modifiche prima dell’esecuzione. Alcuni ambienti di programmazione lo fanno automaticamente, ma altri no.
  • Avete cambiato il nome del file, ma state eseguendo ancora quello con il vecchio nome.
  • Qualcosa nel vostro ambiente di sviluppo non è configurato correttamente.
  • Se state scrivendo un modulo usando import, accertatevi di non dare al vostro modulo lo stesso nome di uno dei moduli standard di Python.
  • Se state usando import per leggere un modulo, ricordatevi che dovete riavviare l’interprete o usare reload per leggere un file modificato. Se importate nuovamente il modulo, non succederà nulla.

Se vi bloccate e non riuscite a capire cosa sta succedendo, un’alternativa è ripartire con un nuovo programma come “Ciao, mondo!”, e accertarvi di avere un programma ben conosciuto da eseguire. Quindi, aggiungete gradualmente i pezzi del programma originale a quello nuovo.

A.2  Errori di runtime

Una volta che il programma è sintatticamente corretto, Python può leggerlo e quantomeno cominciare ad eseguirlo. Cosa può succedere di spiacevole?

A.2.1  Il programma non fa assolutamente nulla.

È il problema più frequente se il vostro file consiste di funzioni e classi, ma in realtà non invoca alcuna funzione per avviare l’esecuzione. Può essere una cosa intenzionale, se intendete utilizzarlo solo come modulo da importare allo scopo di fornire le classi e le funzioni.

Se non è questo il caso, assicuratevi che nel programma ci sia una chiamata di funzione e che il flusso di esecuzione la raggiunga (vedete anche il paragrafo “Flusso di esecuzione” più avanti).

A.2.2  Il programma si blocca.

Se un programma si blocca e pare che non stia succedendo nulla, spesso vuol dire che è incappato in un ciclo infinito o in una ricorsione infinita.

  • Se c’è un ciclo particolare dove sospettate che stia il problema, aggiungete un’istruzione di stampa immediatamente prima del ciclo che dice: “Sto entrando nel ciclo” e una immediatamente dopo che dice: “Sto uscendo dal ciclo”.

    Avviate il programma. Se viene visualizzato il primo messaggio ma non il secondo, c’è un ciclo infinito. Proseguite con il Paragrafo “Ciclo infinito” più sotto.

  • Il più delle volte, in presenza di una ricorsione infinita, il programma funziona per qualche tempo per poi produrre un errore RuntimeError: Maximum recursion depth exceeded. Se succede questo, andate al Paragrafo “Ricorsione infinita”.

    Se non ottenete questo errore ma sospettate che ci sia un problema con un metodo o funzione ricorsivi, potete usare ugualmente le tecniche illustrate nel Paragrafo “Ricorsione infinita”.

  • Se nessuno di questi punti funziona, fate delle prove su altri cicli o funzioni e metodi ricorsivi.
  • Se ancora non funziona, forse non avete ben chiaro il flusso di esecuzione del vostro programma. Andate al relativo Paragrafo.

Ciclo infinito

Se sospettate che vi sia un ciclo infinito e di sapere quale ciclo in particolare stia causando il problema, aggiungete un’istruzione di stampa alla fine del ciclo in modo da visualizzare il valore delle variabili nella condizione e il valore della condizione.

Per esempio:

while x > 0 and y < 0 :
    # fa qualcosa con x
    # fa qualcosa con y

    print('x: ', x)
    print('y: ', y)
    print("condizione: ", (x > 0 and y < 0))

Ora, eseguendo il programma, vedrete tre righe di output per ogni ripetizione del ciclo. All’ultimo passaggio, la condizione dovrebbe risultare False. Se il ciclo continua, vedrete comunque i valori di x e y, e potrete capire meglio il motivo per cui non vengono aggiornati correttamente.

Ricorsione infinita

Il più delle volte, una ricorsione infinita provoca un errore di Maximum recursion depth exceeded, dopo che il programma è stato in esecuzione per qualche istante.

Se sospettate che una funzione stia provocando una ricorsione infinita, controllate innanzitutto che esista un caso base. Deve esistere un qualche tipo di condizione che provoca il ritorno della funzione senza generare un’altra chiamata ricorsiva. Se no, occorre ripensare l’algoritmo e stabilire un caso base.

Se il caso base c’è ma sembra che il programma non lo raggiunga mai, aggiungete un’istruzione di stampa all’inizio della funzione, in modo da visualizzare i parametri. Ora, eseguendo il programma vedrete alcune righe di output con i valori dei parametri per ogni chiamata della funzione. Se i parametri non tendono verso il caso base, avrete qualche spunto per capire il perché.

Flusso di esecuzione

Se non siete sicuri di come il flusso di esecuzione si muova dentro il vostro programma, aggiungete delle istruzioni di stampa all’inizio di ogni funzione con un messaggio del tipo “sto eseguendo la funzione pippo”, dove pippo è il nome della funzione.

Ora, eseguendo il programma, verrà stampata una traccia di ogni funzione che viene invocata.

A.2.3  Quando eseguo il programma è sollevata un’eccezione.

Se qualcosa non va durante l’esecuzione, Python stampa un messaggio che include il nome dell’eccezione, la riga del programma dove il problema si è verificato, ed un traceback.

Il traceback identifica la funzione che era in esecuzione, quella che l’aveva chiamata, quella che a sua volta l’aveva chiamata e così via. In altre parole, traccia la sequenza di chiamate di funzione che hanno condotto alla situazione attuale. Include anche il numero di riga del file dove è avvenuta ciascuna chiamata.

Innanzitutto bisogna esaminare il punto del programma dove è emerso l’errore e vedere se si riesce a capire cosa è successo. Questi sono alcuni dei più comuni errori in esecuzione:

NameError:
State cercando di usare una variabile che non esiste nell’ambiente attuale. Controllate che il nome sia scritto esattamente e che sia coerente. Ricordate anche che le variabili locali sono, per l’appunto, locali: non potete fare loro riferimento dall’esterno della funzione in cui sono definite.
TypeError:
Ci sono alcune possibili cause:
  • State cercando di usare un valore in modo improprio. Esempio: indicizzare una stringa, lista o tupla con qualcosa di diverso da un numero intero.
  • C’è una mancata corrispondenza tra gli elementi in una stringa di formato e gli elementi passati per la conversione. Succede se il numero degli elementi non corrisponde o se viene tentata una conversione non valida.
  • State passando a una funzione un numero sbagliato di argomenti. Per i metodi, guardatetene la definizione e controllate che il primo parametro sia self. Quindi guardate l’invocazione: assicuratevi di invocare il metodo su un oggetto con il giusto tipo e di fornire correttamente gli altri argomenti.
KeyError:
State tentando di accedere a un elemento di un dizionario usando una chiave che nel dizionario non esiste. Se le chiavi sono delle stringhe, ricordate che c’è differenza tra lettere maiuscole e minuscole.
AttributeError:
State tentando di accedere a un attributo o a un metodo che non esiste. Controllate se è scritto giusto! Potete usare la funzione predefinita vars per elencare gli attributi esistenti.

Se un AttributeError indica che un oggetto è di tipo NoneType, vuol dire che è None. Il problema non è il nome dell’attributo, ma l’oggetto. la ragione per cui un oggetto è None può essere dimenticare di ritornare un valore da una funzione: se arrivate in fondo a una funzione senza intercettare un’istruzione return, questa restituisce None. Un’altra causa frequente è usare il risultato di un metodo di una lista, come sort, che restituisce None.

IndexError:
L’indice che state usando per accedere a una lista, stringa o tupla è maggiore della lunghezza della sequenza meno uno. Immediatamente prima dell’ubicazione dell’errore aggiungete un’istruzione di stampa che mostri il valore dell’indice e la lunghezza della struttura. Quest’ultima è della lunghezza esatta? E l’indice ha il valore corretto?

Il debugger Python (pdb) è utile per catturare le eccezioni, perché vi permette di esaminare lo stato del programma immediatamente prima dell’errore. Potete leggere approfondimenti su pdb sul sito https://docs.python.org/3/library/pdb.html.

A.2.4  Ho aggiunto talmente tante istruzioni di stampa che sono sommerso di output.

Una controindicazione delle istruzioni di stampa nel debugging è che si rischia di essere inondati di messaggi. Ci sono due modi di procedere: o semplificate l’output o semplificate il programma.

Per semplificare l’output, potete rimuovere o commentare le istruzioni di stampa superflue, o accorparle, o dare all’output un formato più leggibile.

Per semplificare il programma, ci sono diverse opzioni. Primo, riducete il problema che il programma sta affrontando. Per esempio, se state cercando una lista, cercatene una piccola. Se il programma riceve input dall’utente, dategli il dato più semplice che causa il problema.

Secondo, ripulite il programma. Togliete il codice inutile e riorganizzate il programma in modo da renderlo il più facile possibile da leggere. Per esempio, se sospettate che l’errore sia in una parte profondamente nidificata del programma, cercate di riscrivere quella parte con una struttura più semplice. Se sospettate di una corposa funzione, provate a suddividerla in funzioni più piccole e a testarle separatamente.

Spesso, il procedimento di ricercare il caso di prova più circoscritto porta a trovare l’errore. Se riscontrate che il programma funziona in un caso ma non in un altro, questo vi dà una chiave di lettura di quello che sta succedendo.

Allo stesso modo, riscrivere un pezzo di codice vi può aiutare a trovare errori insidiosi. Una modifica che pensavate ininfluente sul programma, e che invece influisce, può farvi trovare il bandolo della matassa.

A.3  Errori di semantica

Gli errori di semantica sono i più difficili da affrontare, perché l’interprete non fornisce informazioni su ciò che non va. Sapete per certo solo quello che il programma dovrebbe fare, ma non fa.

Innanzitutto occorre stabilire una connessione logica tra il testo del programma e il comportamento che vedete. Vi serve un’ipotesi di cosa sta facendo in realtà il programma. Quello che rende difficili le cose è che i computer eseguono le operazioni in tempi rapidissimi.

Vi capiterà di desiderare di poter rallentare il programma ad una velocità umana, e in effetti con alcuni strumenti di debug potete farlo. Ma il tempo che ci vuole per inserire alcune istruzioni di stampa ben calibrate è spesso più breve di quello necessario a impostare il debugger, inserire e togliere i punti di interruzione, ed eseguire i passi per portare il programma dove si verifica l’errore .

A.3.1  Il mio programma non funziona.

Dovreste porvi queste domande:

  • C’è qualcosa che il programma dovrebbe fare ma che non sembra accadere? Trovate la parte del codice che esegue quella funzione e assicuratevi che sia effettivamente eseguita quando ritenete che dovrebbe esserlo.
  • Sta succedendo qualcosa che non dovrebbe succedere? Trovate il codice che genera quella funzione e controllate se viene eseguita quando invece non dovrebbe esserlo.
  • Una porzione di codice sta producendo effetti inattesi? Assicuratevi di capire il codice in questione, specie se coinvolge funzioni o metodi in altri moduli Python. Leggete la documentazione delle funzioni che chiamate. Provatele scrivendo semplici test e controllando i risultati.

Per programmare, vi serve un modello mentale di come funziona il programma. Se scrivete un programma che non fa quello che volete, spesso il problema non è nel programma ma nel vostro modello mentale.

Il modo migliore per correggere il vostro modello mentale è spezzare il programma nei suoi componenti (di solito funzioni e metodi) e provare indipendentemente ogni singolo componente. Quando avrete trovato la discrepanza tra il vostro modello e la realtà, potrete risolvere il problema.

Naturalmente, dovreste costruire e provare i componenti mentre state sviluppando il programma. Così, se vi imbattete in un problema, dovrebbe esserci solo una piccola quantità di codice di cui occorre verificare l’esattezza.

A.3.2  Ho una grande e complicata espressione che non fa quello che voglio.

Scrivere espressioni complesse va bene fino a quando restano leggibili, ma poi possono diventare difficili da correggere. Un buon consiglio è di spezzare un’espressione complessa in una serie di assegnazioni a variabili temporanee.

Per esempio:

self.mani[i].aggiungiCarta(self.mani[self.trovaVicino(i)].togliCarta())

Può essere riscritta così:

vicino = self.trovaVicino(i)
cartaScelta = self.mani[vicino].togliCarta()
self.mani[i].aggiungiCarta(cartaScelta)

La versione esplicita è più leggibile perché i nomi delle variabili aggiungono informazione, ed è più facile da correggere perché potete controllare i tipi delle variabili intermedie e visualizzare il loro valore.

Un altro problema che si verifica con le grandi espressioni è che l’ordine di valutazione delle operazioni può essere diverso da quello che pensate. Per esempio, nel tradurre in Python l’espressione x/2 π, potreste scrivere:

y = x / 2 * math.pi

È sbagliato, perché moltiplicazione e divisione hanno la stessa priorità e vengono calcolate da sinistra verso destra; quindi quell’espressione calcola x π / 2.

Un buon modo di fare il debug delle espressioni è aggiungere delle parentesi per rendere esplicito l’ordine delle operazioni.

 y = x / (2 * math.pi)

Usate le parentesi ogni volta che non siete certi dell’ordine delle operazioni. Non solo il programma sarà corretto (nel senso che farà quello che volete), sarà anche più leggibile da altre persone che non hanno imparato a memoria l’ordine delle operazioni

A.3.3  Ho una funzione che non restituisce quello che voglio.

Se avete un’istruzione return associata ad un’espressione complessa, non avete modo di stampare il risultato prima del ritorno. Di nuovo, usate una variabile temporanea. Per esempio, anziché:

return self.mani[i].togliUguali()

potete scrivere:

conta = self.mani[i].togliUguali()
return conta

Ora potete stampare il valore di conta prima che sia restituito.

A.3.4  Sono proprio bloccato e mi serve aiuto.

Per prima cosa, staccatevi dal computer per qualche minuto. I computer emettono onde che influenzano il cervello, causando questi sintomi:

  • Frustrazione e rabbia.
  • Credenze superstiziose (“il computer mi odia”) e influssi magici (“il programma funziona solo quando indosso il berretto all’indietro”).
  • Programmazione a tentoni (il tentativo di programmare scrivendo ogni possibile programma e prendendo quello che funziona).

Se accusate qualcuno di questi sintomi, alzatevi e andate a fare una passeggiata. Quando vi siete calmati, ripensate al programma. Cosa sta facendo? Quali sono le possibili cause del suo comportamento? Quand’era l’ultima volta che avete avuto un programma funzionante, e cosa avete fatto dopo?

A volte per trovare un bug è richiesto solo del tempo. Io trovo spesso bug mentre non sono al computer e distraggo la mente. Tra i posti migliori per trovare bug: in treno; sotto la doccia; a letto appena prima di addormentarsi.

A.3.5  No, ho davvero bisogno di aiuto.

Capita. Anche i migliori programmatori a volte si bloccano. Magari avete lavorato talmente a lungo sul programma da non riuscire a vedere un errore. Un paio di occhi freschi sono quello che ci vuole.

Prima di rivolgervi a qualcun altro, dovete fare dei preparativi. Il vostro programma dovrebbe essere il più semplice possibile, e dovete fare in modo di lavorare sul più circoscritto input che causa l’errore. Dovete posizionare delle istruzioni di stampa nei posti adatti (e l’output che producono deve essere comprensibile). Il problema va compreso abbastanza bene da poterlo descrivere in poche parole.

Quando portate qualcuno ad aiutarvi, assicuratevi di dargli tutte le informazioni che servono:

  • Se c’è un messaggio di errore, di cosa si tratta e quale parte del programma indica?
  • Qual è l’ultima cosa che avete fatto prima della comparsa dell’errore? Quali erano le ultime righe di codice che avevate scritto, oppure il nuovo caso di prova che non è riuscito?
  • Cosa avete provato a fare finora, e cosa avete appreso dai tentativi?

Quando trovate l’errore, prendetevi un attimo di tempo per pensare cosa avreste potuto fare per trovarlo più velocemente: la prossima volta che incontrerete qualcosa di simile, vi sarà più facile scoprire l’errore.

Ricordate che lo scopo non è solo far funzionare il programma, ma imparare a farlo funzionare.

Buy this book at Amazon.com

Contribute

If you would like to make a contribution to support my books, you can use the button below. Thank you!
Pay what you want:

Are you using one of our books in a class?

We'd like to know about it. Please consider filling out this short survey.



Previous Up Next