Software in pratica

 ☗   ◀   ☰ 

LA GESTIONE DEGLI ERRORI

Ci sono tre tipi di errori in cui potete incorrere. Il primo sono gli errori di logica, dovuti ad un'errata analisi del problema o al procedimento descritto e codificato in modo errato. Questi errori sono totalmente a carico vostro, il computer in fondo esegue con la massima precisione quello che gli dite di fare, e non può in alcun modo sapere se è qualcosa di sensato oppure no.

Il secondo tipo di errore sono le innumerevoli situazioni inaspettate che si possono incontrare durante l'esecuzione di un programma (a runtime), come il tentativo di aprire un file inesistente, effettuare una divisione per zero, tentare di accedere a elementi inesistenti di una lista, effettuare operazioni matematiche con dati non numerici, per tutte queste cose e altre ancora l'interprete Python "solleva delle eccezioni", che se non gestite in qualche modo fanno terminare immediatamente il programma e apparire un bruttissimo messaggio sulla console... così brutto che i principianti non lo leggono mai con attenzione, ma se lo leggessero vedrebbero che di solito il tipo di errore, e il punto in cui si verifica, è descritto in modo abbastanza esplicativo.

Il terzo tipo di errore è il banale errore sintattico, istruzioni scritte in modo errato, parentesi o virgolette non chiuse, indentazioni errate, simboli mancanti dove richiesti o presenti dove non dovrebbero esserci, questi errori di solito impediscono del tutto la partenza del programma già dall'inizio, per cui sono i più facili da individuare e vengono corretti durante lo sviluppo del programma stesso.

I linguaggi moderni prevedono delle apposite istruzioni per far fronte, nel limite del possibile, gli errori del secondo tipo. Ad esempio vi sarete sicuramente accorti già con le prime prove che usare la funzione int(input()) non è sicuro, se scrivete qualcosa che non sia un numero valido, o se battete invio senza aver scritto niente, il programma si blocca e sulla console appare un messaggio di errore. Per far fronte a queste eventualità, e avere la possibilità di "recuperare" la situazione, dovete usare gli statement try ed except in questo modo:

try:
    ....istruzioni protette....
except TipoDiErrore:
    ....istruzioni da eseguire se avviene
    ....un errore nelle istruzioni protette

Ecco come potete scrivere una funzione di input numerico intero "sicura" (che potete sostituire in tutti gli esempi precedenti), che in caso di errore non causa l'arresto del programma ma richiede semplicemente un'altra volta l'immissione del dato (con una scritta che qualcuno ricorderà con nostalgia).

def int_input(m):
    while True:
        try:
            return int(input(m))
        except ValueError:
            print("?REDO FROM START")

a = int_input("ANNO? ")
Un altro esempio:
try:
    a = 5 / 0
    print("Finito")
except ZeroDivisionError as x:
    print(x)
Nota: in vecchie versioni di Python 2.x sostituire ZeroDivisionError as x: con ZeroDivisionError, x:

Dopo except potete omettere il tipo di eccezione in modo da "catturare" qualsiasi eccezione, ma questo non solo può rendere difficile scoprire che tipo di errore è avvenuto, può anche impedirvi di interrompere il programma con control + C, pertanto è una cosa generalmente da non fare.


DEFINIRE NUOVE ECCEZIONI

Potete definire eccezioni vostre derivando una classe dalla classe predefinita Exception, e le potete "sollevare" (raise) come un qualsiasi altro errore runtime di Python. Al momento della raise oltre alla vostra classe potete aggiungere dei dati che si possono assegnare ad una variabile nel gestore dell'eccezione:
class MiaEccezione(Exception):
    pass # Nessun metodo e nessun attributo

#-------------------------------------------------------------------------


def pippo():
    print("  Nella funzione pippo")
    raise MiaEccezione("eccezziunale veramente")
    print("questo non viene stampato")

#-------------------------------------------------------------------------

try:
    print("Chiamo pippo")
    pippo()
    print("neanche questo viene stampato")
except MiaEccezione as dati:
    print("Si e' verificato un errore", dati)

Chi gestisce l'eccezione? Il primo blocco try/except utile. Se è dentro una funzione la gestione avverrà all'interno della funzione, altrimenti la palla passa al chiamante di quella funzione, se neppure li vi è un adeguato blocco try/except si passa all'eventuale ulteriore chiamante e così via. Se, risalendo la catena delle chiamate, si arriva al blocco principale del programma e neppure li c'è un gestore, il programma termina con la segnalazione dell'errore sulla console.


LE ECCEZIONI NON SONO SEMPRE ERRORI

In Python le eccezioni non servono solo per far fronte a veri e propri errori, sono anche un modo strutturato per gestire situazioni, lasciando all'interptere il compito di stabilire se una cosa può andare a buon fine in un certo modo o se è necessario ricorrere ad un'alternativa (duck typing).

Nell'esempio seguente c'è una funzione di immissione stringa compatibile sia con Python 2 che con Python 3. Si prova prima a chiamare la funzione di input stringa specifica di Python 2, se la funzione non viene trovata si richiama a funzione di input stringa specifica di Python 3

# Ritorna una stringa indipendentemente dalla versione di Python usata
def s_input(messaggio):
    try:
        return = raw_input(messaggio) # Eseguita su Python 2
    except NameError:
        return = input(messaggio)     # Eseguita su Python 3

Con la gestione degli errori si possono quindi realizzare input "sicuri", che non solo non causano l'arresto del programma se si immette qualcosa di non valido, ma che funzionano indipendentemente dalla versione di Python utilizzata. Ad esempio si può riscrivere l'ultimo programma del terzo capitolo (true/break) per usare la funzione s_input appena vista, scomponendolo ulteriormente in due funzioni int_input e int_range_input più generali e riutilizzabili in qualsiasi altro programma:

# Ritorna un numero intero rifiutando inserimenti errati, richiama s_input
def int_input(messaggio):
    while True:
        try:
            return int(s_input(messaggio))
        except ValueError:
            print("Errore: inserire numero intero")


# Ritorna un numero intero solo se compreso tra i limiti inf e sup, richiama int_input
def int_range_input(messaggio, inf, sup):
    while True:
        v = int_input(messaggio)
        if v >= inf  and  v <= sup: 
            return v
        print("Errore: inserire un valore nei limiti previsti")


for x in range(int_range_input("QUANTO TI PIACE PYTHON (DA 1 A 10) ?", 1, 10)):
    print("W PYTHON")


PROGRAMMANDO SI IMPARA

Il modo migliore consolidare tutti i concetti finora incontrati, è analizzare qualche altro esempio pratico... ricordate che è solo programmando che si impara a programmare.

Un classico è il sort (ordinamento) di una sequenza di dati. Fare il sort significa mettere i dati in ordine secondo dei criteri da noi scelti, ad esempio disporre in ordine alfabetico una serie di nomi, o in ordine crescente dal più piccolo al più grande un gruppo di numeri. Il metodo qui presentato si chiama bubble sort, in quanto porta lentamente "a galla" i dati nel loro ordine corretto tramite una serie di confronti e scambi. Prima di ulteriori spiegazioni eccovi il listing del programma:

#------ PROGRAMMA BUBBLE SORT

import random
print("GENERO I NOMI CASUALI")
N_PAR = 10                     # numero parole
L_PAR = 5                      # lunghezza parole
a = []                         # predispone lista parole vuota
for i in range(N_PAR):
    e = ""                     # predispone parola vuota
    for j in range(L_PAR):
        k = random.randint(ord("A"), ord("Z"))
        e = e + chr(k)         # aggiunge carattere alla parola
    a.append(e)                # aggiunge parola alla lista
    print(e)
print()
print("MO' LI METTO IN ORDINE")
for i in range(N_PAR):
    for j in range(N_PAR - 2):
      if a[j] > a[j+1]:        # se a[j]>a[j+1] scambiali
          temp = a[j]
          a[j] = a[j+1]
          a[j+1] = temp
for i in a:                    # ciclo di stampa nomi ordinati
    print(i)

La prima parte del programma crea e stampa una lista di parole casuali in modo simile a quanto visto nel programma aliens. Il sort vero proprio avviene nei cicli nidificati sotto la riga "mo' li metto in ordine". L'algoritmo del bubble sort prevede che si debbano confrontare (ed eventualmente scambiare) il primo elemento con il secondo, il secondo con il terzo e così via fino al penultimo con l'ultimo (questo avviene nel ciclo interno), e che tutto questo debba essere ripetuto tante volte quanti sono gli elementi da ordinare (a questo ci pensa il ciclo esterno).

Nel ciclo interno si fa uso di una "variabile di parcheggio" temp per effettuare lo scambio (swap) tra due elementi della lista senza perderne il contenuto. Alla fine vengono stampati gli elementi della lista che risultano ordinati in ordine alfabetico crescente. Come si può vedere è lecito effettuare un confronto tra stringhe usando gli operatori relazionali maggiore e minore, perché per i confronti il computer considera il codice numerico con cui sono codificati i caratteri, le lettere minuscole vengono prima delle maiuscole, e i caratteri numerici vengono prima delle lettere.


QUICK SORT

Esistono altri algoritmi di ordinamento, come il quick sort, molto più veloce del bubble sort quando i dati da ordinare sono molti. Il quick sort è particolarmente adatto ad essere scritto in forma ricorsiva.

Il principio del quick sort è semplice, prendo un elemento di una lista (ad esempio il primo) e lo chiamo pivot (perno), poi creo due liste, una contenente tutti gli elementi minori o uguali, e un'altra con tutti gli elementi maggiori del pivot. A questo punto tratto ogni sottolista allo stesso modo, scelgo un pivot e creo altre due sottoliste. Continuando nella suddivisione si raggiunge il punto in cui ogni sottolista è formata solamente da uno o nessun elemento, e tutte le sottoliste messe in fila, in ordine assieme ai relativi pivot, rappresentano la lista iniziale completamente ordinata. La soluzione ricorsiva è semplice e compatta:

#------ PROGRAMMA QUICK SORT

def qs(x):
    le = len(x)
    if le > 1:                      # se > di 1 elemento
        sx = []                     # crea sottolista sinistra
        dx = []                     # crea sottolista destra
        pv = x[0]                   # stabilisce pivot l'indice 0
        for i in range(le):         # ciclo suddivisione elementi
            if i != 0:              # suddivide tutto tranne il pivot
                if x[i] <= pv:
                    sx.append(x[i])
                else:
                    dx.append(x[i])
        x = qs(sx) + [pv] + qs(dx)  # calcola ricorsivamente risultato
    return x                        # ritorna il risultato

#---------------------------------------------------------------------

l = [600, 34, 890, 20, 1, 3000, 82, 99, 1200, 8, 15]
print("LISTA ORDINATA")
for e in qs(l):
    print(e)

In questo esempio qs è la funzione di quick sort, accetta come parametro una lista da ordinare, e restituisce come valore di ritorno una lista ordinata. Nel caso in cui la lista da ordinare sia formata da zero o un elemento viene ritornata la lista immutata. Nel caso sia composta da più di un elemento vengono create le due sottoliste sx e dx, viene stabilito il pivot ed infine tramite un ciclo che scorre tutta la lista (escluso il pivot) si popolano le due sottoliste.

A questo punto si imposta come valore di ritorno una lista formata dalla concatenazione di tre liste, il pivot al centro, e due liste ai lati che vengono ordinate chiamando di nuovo (ricorsivamente) la funzione qs.


MASTER MIND

Il seguente programma mostra come un notissimo gioco si possa computerizzare. Il gioco consiste nell'indovinare un codice segreto tramite i messaggi forniti dall'avversario. Il codice segreto è composto da quattro cifre casuali con valore da 1 a 6 (possono esserci più cifre uguali nello stesso codice). Il computer risponderà usando due numeri (chiamati neri e bianchi), il primo indica quante cifre del vostro tentativo hanno giusto valore e si trovano nell'esatta posizione, il secondo quante delle rimanenti sono corrette come valore ma non come posizione.

#------ PROGRAMMA MASTER MIND

import random
codi = []
for h in range(4):
    codi.append(random.randint(1, 6))
n = 0
while True:
    neri = 0
    bianchi = 0
    n = n + 1
    tent = []
    d = input("TENTA: ")
    for e in d:
        tent.append(int(e))
    print(" "*20 + "TENTATIVO: " + d + "   ")
    for i in range(4):
        if codi[i] == tent[i]:
            neri = neri + 1
            tent[i] = 0
        else:
            for k in range(4):
                if codi[i] == tent[k] and tent[k] != codi[k]:
                    bianchi = bianchi + 1
                    tent[k] = 0
                    break
    print("NERI =", neri, "  BIANCHI =", bianchi)
    if neri == 4: break
print("ESATTO, IN", n, "TENTATIVI")

Questo programma è il più complesso tra tutti quelli visti finora, usa praticamente tutto quello di cui abbiamo parlato, cicli, condizioni, operatori logici, funzioni di stringa con concatenazione e conversione, liste con accesso ad indice agli elementi, iterazioni con for, e in più, oltre queste caratteristiche sintattiche, ha una propria logica di funzionamento abbastanza complessa che probabilmente richiede una bella simulazione su un foglio di carta per essere compresa appieno.

Come aiuto va ricordato che le operazioni tent[i]=0 e tent[k]=0 servono per far si che gli elementi trovati corrispondenti tra codice segreto e tentativo non vengano più presi in considerazione durante le rimanenti iterazioni dei cicli dei controlli.



PROGRAMMAZIONE STRUTTURATA

Con questo termine si intende una metodologia di strutturare i programmi che segue delle semplici regole per ottenere programmi facilmente leggibili, modificabili e, in sostanza, con meno errori. La programmazione strutturata prevede di organizzare il flusso di esecuzione del programma con solo tre tipi di strutture (sequenziale, decisionale, iterativa) con un solo punto di ingresso e un solo punto di uscita per ogni struttura.

Le strutture possono essere annidate arbitrariamente una dentro l'altra: dentro un ciclo possono esserci degli if contenenti altri cicli o altri if e così via, ma alla fine si passa sempre per l'uscita della struttura principale. Nella programmazione strutturata non è ammesso saltare arbitrariamente da un punto all'altro del programma, ed in effetti in Python non esiste nessuna istruzione "GOTO".

Tutti gli esempi mostrati fin qui sono stati scritti in modo strutturato, che è poi l'unico possibile e naturale in Python. L'indentazione obbligatoria del codice evidenzia ancora meglio la struttura del programma, ed è pensata non solo per definirne sintatticamente l'ordine di esecuzione, ma soprattutto per la lettura da parte di un essere umano. Tutte queste cose rendono il codice dei programmi Python molto chiaro e abituano da subito ad un corretto stile di scrittura.

Si può considerare parte della programmazione strutturata anche la suddivisione ragionata del programma in piccole unità funzionali che svolgono compiti ben precisi e indipendenti. Quando ci si trova a scrivere strutture troppo lunghe e nidificate (magari lunghe diverse pagine e quindi difficili anche da leggere) probabilmente si sta sbagliando qualcosa, perché sicuramente quella logica può essere scomposta e ripensata in termini di funzioni più semplici da chiamare, oppure classi per semplificare la gestione. In genere non si dovrebbero mai scrivere funzioni o strutture più lunghe di una videata da 30..40 righe.


IN CONCLUSIONE... QUESTO È SOLO L'INIZIO

Se siete giunti fino qua partendo da zero e comprendendo tutto complimenti davvero, avete percorso otto passi che hanno toccato progressivamente quasi tutti i punti fondamentali, ritrovabili in forme simili in ogni linguaggo moderno (e anche meno moderno)... e questo era lo scopo di questo testo.

Ho volutamente sorvolato su molti dettagli, a parte quelli indispensabili per gli esempi, cercando di rimanere sul pratico, con programmi su cui sperimentare piuttosto che con divagazioni teoriche. Avete visto quello che si potrebbe chiamare "il nucleo della programmazione", dai primi concetti di dati istruzioni e variabili, agli operatori relazionali e logici per creare strutture condizionali e iterative con cui controllare il flusso dell'esecuzione, alla suddivisione dei programmi in funzioni/sottoprogrammi, comprese alcune funzioni per la manipolazione simbolica delle stringhe, all'uso delle liste come contenitori di dati, con accenni alla ricorsione, all'organizzazione successiva dei programmi in oggetti e al trattamento degli errori di esecuzione.

Ma Python (e qualsiasi altro linguaggio) è molto di più, ora si deve infatti ripartire da capo, per approfondire i tipi di dati e le operazioni che si possono compiere, le funzioni predefinite principali, la visibilità delle variabili e i diversi modi di passaggio degli argomenti, i metodi delle stringhe e la loro formattazione avanzata, le caratteristiche aggiuntive delle strutture if e while, i test di verità sugli oggetti, gli altri metodi delle liste e delle stringhe, le altre strutture dati come tuple dizionari set e bytes. E poi le funzioni e gli oggetti contenute negli innumerevoli moduli, tra cui quelli per la creazione di interfacce grafiche (GUI). La programmazione di GUI richiede poi il passaggio dall' idea del programma guidato dal flusso logico delle istruzioni (come avete visto fino ad adesso) a quella del programma guidato dagli eventi prodotti dall'utente (event driven), e apre le porte all'animazione e al tracciamento di grafici e funzioni.

Ma in tutto questo sarete facilitati dal fatto che ormai i "mostri sacri" li avete già incontrati, e potrete sempre rifarvi alle cose già conosciute.



RINGRAZIAMENTI

Si ringraziano per la segnalazione di orrori:
 ☗   ◀   ☰