Previous Up Next

Buy this book at Amazon.com

Chapter 16  Classi e funzioni

Ora che sappiamo come creare dei nuovi tipi, il passo successivo è scrivere delle funzioni che prendano i tipi personalizzati come parametri e restituiscano dei risultati. In questo capitolo presenterò anche lo “stile di programmazione funzionale” e due nuove tecniche di sviluppo.

Il codice degli esempi di questo capitolo è scaricabile dal sito http://thinkpython2.com/code/Time1.py. Le soluzioni degli esercizi si trovano qui: http://thinkpython2.com/code/Time1_soln.py.

16.1  Tempo

Facciamo un altro esempio di tipo personalizzato, creato dal programmatore, e definiamo una classe chiamata Tempo che permette di rappresentare un’ora del giorno:

class Tempo:
    """Rappresenta un'ora del giorno.
       
    attributi: ora, minuto, secondo
    """

Possiamo creare un nuovo oggetto Tempo, assegnandogli tre attributi per le ore, i minuti e i secondi:

tempo = Tempo()
tempo.ora = 11
tempo.minuto = 59
tempo.secondo = 30

Il diagramma di stato dell’oggetto Tempo è riportato in Figura 16.1.

Provate ora a scrivete una funzione di nome stampa_tempo che accetti un oggetto Tempo come argomento e ne stampi il risultato nel formato ore:minuti:secondi. Suggerimento: la sequenza di formato '%.2d' stampa un intero usando almeno due cifre, compreso uno zero iniziale dove necessario.

Scrivete poi una funzione booleana viene_dopo che riceva come argomenti due oggetti Tempo, t1 e t2, e restituisca True se t1 è temporalmente successivo a t2 e False in caso contrario. Opzione più difficile: non usate un’istruzione if.


Figure 16.1: Diagramma di oggetto.

16.2  Funzioni pure

Nei prossimi paragrafi scriveremo due funzioni che sommano dei valori, espressi in termini temporali. Illustreremo così due tipi di funzioni: le funzioni pure e i modificatori. Dimostreremo anche una tecnica di sviluppo che chiameremo prototipo ed evoluzioni, che è un modo di affrontare un problema complesso partendo da un prototipo semplice e trattando poi in maniera incrementale gli aspetti di maggior complessità.

Ecco un semplice prototipo della funzione somma_tempo:

def somma_tempo(t1, t2):
    somma = Tempo()
    somma.ora = t1.ora + t2.ora
    somma.minuto = t1.minuto + t2.minuto
    somma.secondo = t1.secondo + t2.secondo
    return somma

La funzione crea un nuovo oggetto Tempo, ne inizializza gli attributi, e restituisce un riferimento al nuovo oggetto. Questa è detta funzione pura, perché non modifica alcuno degli oggetti che le vengono passati come argomento e, oltre a restituire un valore, non ha effetti visibili come visualizzare valori o chiedere input all’utente.

Per provare questa funzione, creiamo due oggetti Tempo: inizio che contiene l’ora di inizio di un film, come I Monty Python e il Sacro Graal, e durata che contiene la durata del film, che è un’ora e 35 minuti.

somma_tempo ci dirà a che ora finisce il film.

>>> inizio = Tempo()
>>> inizio.ora = 9
>>> inizio.minuto = 45
>>> inizio.secondo =  0

>>> durata = Tempo()
>>> durata.ora = 1
>>> durata.minuto = 35
>>> durata.secondo = 0

>>> fine = somma_tempo(inizio, durata)
>>> stampa_tempo(fine)
10:80:00

Il risultato, 10:80:00 non è soddisfacente. Il problema è che questa funzione non gestisce correttamente i casi in cui la somma dei minuti e dei secondi equivale o supera sessanta. Quando questo accade, dobbiamo “riportare” i 60 secondi come minuto ulteriore, o i 60 minuti come ora ulteriore.

Ecco allora una versione migliorata della funzione:

def somma_tempo(t1, t2):
    somma = Tempo()
    somma.ora = t1.ora + t2.ora
    somma.minuto = t1.minuto + t2.minuto
    somma.secondo = t1.secondo + t2.secondo

    if somma.secondo >= 60:
        somma.secondo -= 60
        somma.minuto += 1

    if somma.minuto >= 60:
        somma.minuto -= 60
        somma.ora += 1

    return somma

Sebbene questa funzione sia corretta, comincia ad essere lunga. Tra poco vedremo un’alternativa più concisa.

16.3  Modificatori

Ci sono casi in cui è utile che una funzione possa modificare gli oggetti che assume come parametri. I cambiamenti risulteranno visibili anche al chiamante. Funzioni che si comportano in questo modo sono dette modificatori.

incremento, che aggiunge un dato numero di secondi ad un oggetto Tempo, può essere scritta intuitivamente come modificatore. Ecco un primo abbozzo della funzione:

def incremento(tempo, secondi):
    tempo.secondo += secondi

    if tempo.secondo >= 60:
        tempo.secondo -= 60
        tempo.minuto += 1

    if tempo.minuto >= 60:
        tempo.minuto -= 60
        tempo.ora += 1

La prima riga esegue l’operazione di addizione fondamentale, mentre le successive controllano i casi particolari che abbiamo già visto prima.

Questa funzione è corretta? Cosa succede se secondi è molto più grande di 60?

In questo caso non è più sufficiente un unico riporto tra secondi e minuti: dobbiamo fare in modo di ripetere il controllo più volte, finché tempo.secondo diventa minore di 60. Allora, una possibile soluzione è quella di sostituire le istruzioni if con delle istruzioni while. Questo renderebbe la funzione corretta, ma non molto efficiente.

Come esercizio, scrivete una versione corretta di incremento che non contenga alcun ciclo.

Tutto quello che può essere fatto con i modificatori può anche essere fatto con le funzioni pure. Tanto è vero che alcuni linguaggi di programmazione prevedono unicamente l’uso di funzioni pure. Si può affermare che i programmi che utilizzano funzioni pure sono più veloci da sviluppare e meno soggetti ad errori rispetto a quelli che fanno uso dei modificatori. Ma in qualche caso i modificatori convengono, perché i programmi funzionali risultano meno efficienti.

In linea generale, raccomando di usare funzioni pure quando possibile e usare i modificatori solo se c’è un evidente vantaggio nel farlo. Questo tipo di approccio può essere definito stile di programmazione funzionale.

Per esercizio, scrivete una versione “pura” di incremento che crei e restituisca un nuovo oggetto Tempo anziché modificare il parametro.

16.4  Sviluppo prototipale e Sviluppo pianificato

La tecnica di sviluppo del programma che sto illustrando in questo Capitolo è detta “prototipo ed evoluzioni”: per ogni funzione, si inizia scrivendo una versione grezza (prototipo) che effettui solo i calcoli fondamentali, provandola e via via migliorandola e correggendo gli errori.

Sebbene questo approccio possa essere abbastanza efficace, specie se non avete una adeguata conoscenza del problema, può condurre a scrivere del codice inutilmente complesso (perché deve affrontare molti casi particolari) e poco affidabile (dato che è difficile essere certi che tutti gli errori siano stati rimossi).

Un’alternativa è lo sviluppo pianificato, nel quale una conoscenza approfondita degli aspetti del problema da affrontare rende la programmazione molto più semplice. Nel nostro caso, questa conoscenza sta nel fatto che l’oggetto Tempo è rappresentabile da un numero a tre cifre in base numerica 60! (vedere http://it.wikipedia.org/wiki/Sistema_sessagesimale.) L’attributo secondo è la “colonna delle unità”, l’attributo minuto è la “colonna delle sessantine”, e l’attributo ora quella della “trecentosessantine”.

Quando abbiamo scritto somma_tempo e incremento, stavamo a tutti gli effetti calcolando una addizione in base 60, e questo è il motivo per cui dovevamo gestire i riporti tra secondi e minuti e tra minuti e ore.

Questa osservazione ci suggerisce un altro tipo di approccio al problema: possiamo convertire l’oggetto Tempo in un numero intero e approfittare della capacità del computer di effettuare operazioni sui numeri interi.

Questa funzione converte Tempo in un intero:

def tempo_in_int(tempo):
    minuti = tempo.ora * 60 + tempo.minuto
    secondi = minuti * 60 + tempo.secondo
    return secondi

E questa è la funzione inversa, che converte un intero in un Tempo (ricordate che divmod divide il primo argomento per il secondo e restituisce una tupla che contiene il quoziente e il resto).

def int_in_tempo(secondi):
    tempo = Tempo()
    minuti, tempo.secondo = divmod(secondi, 60)
    tempo.ora, tempo.minuto = divmod(minuti, 60)
    return tempo

Per convincervi della esattezza di queste funzioni, pensateci un po’ su e fate qualche prova. Una maniera di collaudarle è controllare che tempo_in_int(int_in_tempo(x)) == x per vari valori di x. Questo è un esempio di controllo di coerenza.

Quando vi siete convinti, potete usarle per riscrivere somma_tempo:

def somma_tempo(t1, t2):
    secondi = tempo_in_int(t1) + tempo_in_int(t2)
    return int_in_tempo(secondi)

Questa versione è più concisa dell’originale e più facile da verificare.

Come esercizio, riscrivete incremento usando tempo_in_int e int_in_tempo.

Sicuramente, la conversione numerica da base 60 a base 10 e viceversa è più astratta e meno immediata rispetto al lavoro diretto con i tempi, che è istintivamente migliore.

Ma avendo l’intuizione di trattare i tempi come numeri in base 60, e investendo il tempo necessario per scrivere le funzioni di conversione (tempo_in_int e int_in_tempo), abbiamo ottenuto un programma molto più corto, facile da leggere e correggere, e più affidabile.

Risulta anche più semplice aggiungere nuove caratteristiche, in un secondo tempo. Ad esempio, immaginate di dover sottrarre due Tempi per determinare l’intervallo trascorso. L’approccio iniziale avrebbe reso necessaria l’implementazione di una sottrazione con il prestito. Invece, con le funzioni di conversione, è molto più facile e rapido avere un programma corretto.

Paradossalmente, qualche volta rendere un problema più difficile (o più generale) lo rende più semplice, perché ci sono meno casi particolari da gestire e minori possibilità di errore.

16.5  Debug

Un oggetto Tempo è ben impostato se i valori di minuto e secondo sono compresi tra 0 e 60 (zero incluso ma 60 escluso) e se ora è positiva. ora e minuto devono essere interi, ma potremmo anche permettere a secondo di avere una parte decimale.

Requisiti come questi sono detti invarianti perché devono essere sempre soddisfatti. In altre parole, se non sono soddisfatti significa che qualcosa non è andato per il verso giusto.

Scrivere del codice per controllare le invarianti può servire a trovare errori e a identificarne le cause. Per esempio, potete scrivere una funzione tempo_valido che prende un oggetto Tempo e restituisce False se viola un’invariante:

def tempo_valido(tempo):
    if tempo.ora < 0 or tempo.minuto < 0 or tempo.secondo < 0:
        return False
    if tempo.minuto >= 60 or tempo.secondo >= 60:
        return False
    return True

All’inizio di ogni funzione, potete controllare l’argomento per assicurarvi della sua validità:

def somma_tempo(t1, t2):
    if not tempo_valido(t1) or not tempo_valido(t2):
        raise ValueError, 'oggetto Tempo non valido in somma_tempo'
    secondi = tempo_in_int(t1) + tempo_in_int(t2)
    return int_in_tempo(secondi)

Oppure potete usare un’istruzione assert, che controlla una data invariante e solleva un’eccezione in caso di difetti:

def somma_tempo(t1, t2):
    assert tempo_valido(t1) and tempo_valido(t2)
    secondi = tempo_in_int(t1) + tempo_in_int(t2)
    return int_in_tempo(secondi)

Le istruzioni assert sono utili perché permettono di distinguere il codice che tratta le condizioni normali da quello che controlla gli errori.

16.6  Glossario

prototipo ed evoluzioni:
Tecnica di sviluppo del programma a partire da un prototipo che viene gradualmente provato, esteso e migliorato.
sviluppo pianificato:
Tecnica di sviluppo che comporta profonde conoscenze del problema e maggiore pianificazione rispetto allo sviluppo incrementale o per prototipo.
funzione pura:
Funzione che non modifica gli oggetti ricevuti come argomenti. La maggior parte delle funzioni pure sono produttive.
modificatore:
Funzione che cambia uno o più oggetti ricevuti come argomenti. La maggior parte dei modificatori sono vuoti, ovvero restituiscono None.
stile di programmazione funzionale:
Stile di programmazione in cui la maggior parte delle funzioni è pura.
invariante:
Condizione che deve sempre essere vera durante l’esecuzione del programma.
istruzione assert:
Istruzione che controlla una condizione e solleva un’eccezione se fallisce.

16.7  Esercizi

Il codice degli esempi di questo capitolo è scaricabile dal sito http://thinkpython2.com/code/Time1.py; le soluzioni degli esercizi si trovano in http://thinkpython2.com/code/Time1_soln.py.


Esercizio 1  

Scrivete una funzione di nome moltiplica_tempo che accetti un oggetto Tempo e un numero, e restituisca un nuovo oggetto Tempo che contiene il prodotto del Tempo iniziale per il numero.

Usate poi moltiplica_tempo per scrivere una funzione che prenda un oggetto Tempo che rappresenta il tempo finale di una gara, e un numero che rappresenta la distanza percorsa, e restituisca un oggetto Tempo che rappresenta la media di gara (tempo al chilometro).


Esercizio 2  

Il modulo datetime fornisce l’oggetto time, simile all’oggetto Tempo di questo capitolo, ma che contiene un ricco insieme di metodi e operatori. Leggetene la documentazione sul sito http://docs.python.org/3/library/datetime.html.

  1. Usate il modulo datetime per scrivere un programma che ricavi la data odierna e visualizzi il giorno della settimana.
  2. Scrivete un programma che riceva una data di nascita come input e visualizzi l’età dell’utente e il numero di giorni, ore, minuti e secondi che mancano al prossimo compleanno.
  3. Date due persone nate in giorni diversi, esiste un giorno in cui uno ha un’età doppia dell’altro. Questo è il loro “Giorno del Doppio”. Scrivete un programma che prenda due date di nascita e calcoli quando si verifica il “Giorno del Doppio”.
  4. Un po’ più difficile: scrivetene una versione più generale che calcoli il giorno in cui una persona ha n volte l’età di un’altra.

Soluzione: http://thinkpython2.com/code/double.py

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