Definiamo ora una classe chiamata Tempo che permette di registrare un'ora del giorno:
class Tempo:
pass
Possiamo creare un nuovo oggetto Tempo assegnando gli attributi per le ore, i minuti e i secondi:
Time = Tempo()
Time.Ore = 11
Time.Minuti = 59
Time.Secondi = 30
Il diagramma di stato per l'oggetto Time è:
Esercizio: scrivi una funzione StampaTempo che prende un oggetto Tempo come argomento e ne stampa il risultato nella classica forma ore:minuti:secondi.
Esercizio: scrivi una funzione booleana Dopo che prende come argomenti due oggetti Tempo (Tempo1 e Tempo2) e ritorna vero se Tempo1 segue cronologicamente Tempo2 e falso in caso contrario.
Nelle prossime funzioni scriveremo due versioni di una funzione che chiameremo SommaTempi che calcola la somma di due oggetti Tempo. Questo permetterà di mostrare due tipi di funzioni: le funzioni pure e i modificatori.
Questa è una versione grezza di SommaTempi:
def SommaTempi(T1, T2):
Somma = Tempo()
Somma.Ore = T1.Ore + T2.Ore
Somma.Minuti = T1.Minuti + T2.Minuti
Somma.Secondi = T1.Secondi + T2.Secondi
return Somma
La funzione crea un nuovo oggetto Tempo, inizializza i suoi attributi e ritorna un riferimento al nuovo oggetto. Questa viene chiamata funzione pura perché non modifica in alcun modo gli oggetti passati come suoi parametri e non ha effetti collaterali (del tipo richiedere l'immissione di un valore da parte dell'operatore o stampare un valore a video).
Ecco un esempio che mostra come usare questa funzione: creiamo due oggetti Tempo, OraCorrente che contiene l'ora corrente e TempoCottura che indica il tempo necessario a preparare un pasto. Se non hai ancora scritto StampaTempo guarda la sezione 14.2 prima di continuare:
>>> OraCorrente = Tempo()
>>> OraCorrente.Ore = 9
>>> OraCorrente.Minuti = 14
>>> OraCorrente.Secondi= 30
>>> TempoCottura = Tempo()
>>> TempoCottura.Ore = 3
>>> TempoCottura.Minuti = 35
>>> TempoCottura.Secondi = 0
>>> PastoPronto = SommaTempi(OraCorrente, TempoCottura)
>>> StampaTempo(PastoPronto)
La stampa di questo programma è 12:49:30 ed il risultato è corretto. D'altra parte ci sono dei casi in cui il risultato è sbagliato: riesci a pensarne uno?
Il problema è che nella nostra funzione non abbiamo tenuto conto del fatto che le somme dei secondi e dei minuti possono andare oltre il 59. Quando capita questo dobbiamo conteggiare il riporto come facciamo con le normali addizioni.
Ecco una seconda versione corretta della funzione:
def SommaTempi(T1, T2):
Somma = Tempo()
Somma.Ore = T1.Ore + T2.Ore
Somma.Minuti = T1.Minuti + T2.Minuti
Somma.Secondi = T1.Secondi + T2.Secondi
if Somma.Secondi >= 60:
Somma.Secondi = Somma.Secondi - 60
Somma.Minuti = Somma.Minuti + 1
if Somma.Minuti >= 60:
Somma.Minuti = Somma.Minuti - 60
Somma.Ore = Somma.Ore + 1
return Somma
Questa funzione è corretta e comincia ad avere una certa lunghezza. In seguito ti mostreremo un approccio alternativo che ti permetterà di ottenere un codice più corto.
Ci sono dei casi in cui è utile una funzione che possa modificare gli oggetti passati come suoi parametri. Quando questo si verifica la funzione è detta modificatore.
La funzione Incremento che somma un certo numero di secondi a Tempo può essere scritta in modo molto intuitivo come modificatore. La prima stesura potrebbe essere questa:
def Incremento(Tempo, Secondi):
Tempo.Secondi = Tempo.Secondi + Secondi
if Tempo.Secondi >= 60:
Tempo.Secondi = Tempo.Secondi - 60
Tempo.Minuti = Tempo.Minuti + 1
if Tempo.Minuti >= 60:
Tempo.Minuti = Tempo.Minuti - 60
Tempo.Ore = Tempo.Ore + 1
La prima riga calcola il valore mentre le successive controllano che il risultato sia nella gamma di valori accettabili come abbiamo già visto.
Questa funzione è corretta? Cosa succede se il parametro Secondi è molto più grande di 60? In questo caso non è abbastanza il riporto 1 tra secondi e minuti, e quindi dobbiamo riscrivere il controllo per fare in modo di continuarlo finché Secondi non diventa minore di 60. Una possibile soluzione è quella di sostituire l'istruzione if con un ciclo while:
def Incremento(Tempo, Secondi):
Tempo.Secondi = Tempo.Secondi + Secondi
while Tempo.Secondi >= 60:
Tempo.Secondi = Tempo.Secondi - 60
Tempo.Minuti = Tempo.Minuti + 1
while Tempo.Minuti >= 60:
Tempo.Minuti = Tempo.Minuti - 60
Tempo.Ore = Tempo.Ore + 1
La funzione è corretta, ma certamente non si tratta ancora della soluzione più efficiente possibile.
Esercizio: riscrivi questa funzione facendo in modo di non usare alcun tipo di ciclo.
Esercizio: riscrivi Incremento come funzione pura e scrivi delle chiamate di funzione per entrambe le versioni.
Qualsiasi cosa che può essere fatta con i modificatori può anche essere fatta con le funzioni pure e alcuni linguaggi di programmazione non prevedono addirittura i modificatori. Si può affermare che le funzioni pure sono più veloci da sviluppare e portano ad un minor numero di errori, anche se in qualche caso può essere utile fare affidamento sui modificatori.
In generale raccomandiamo di usare funzioni pure quando possibile e usare i modificatori come ultima risorsa solo se c'è un evidente vantaggio nel farlo. Questo tipo di approccio può essere definito stile di programmazione funzionale.
In questo capitolo abbiamo mostrato un approccio allo sviluppo del programma che possiamo chiamare sviluppo prototipale: siamo partiti con lo stendere una versione grezza (prototipo) che poteva effettuare solo i calcoli di base, migliorandola e correggendo gli errori man mano che questi venivano trovati.
Sebbene questo approccio possa essere abbastanza efficace può portare a scrivere un codice inutilmente complesso (perché ha a che fare con molti casi speciali) e inaffidabile (dato che è difficile sapere se tutti gli errori sono stati rimossi).
Un'alternativa è lo sviluppo pianificato nel quale lo studio preventivo del problema da affrontare rende la programmazione molto più semplice. Nel nostro caso potremmo accorgerci che a tutti gli effetti l'oggetto Tempo è rappresentabile da tre cifre in base numerica 60.
Quando abbiamo scritto SommaTempi e Incremento stavamo effettivamente calcolando una somma in base 60 e questo è il motivo per cui dovevamo gestire il riporto tra secondi e minuti, e tra minuti e ore quando la somma era maggiore di 59.
Questa osservazione ci suggerisce un altro tipo di approccio al problema: possiamo convertire l'oggetto Tempo in un numero singolo ed avvantaggiarci del fatto che il computer lavora bene con le operazioni aritmetiche. Questa funzione converte un oggetto Tempo in un intero:
def ConverteInSecondi(Orario):
Minuti = Orario.Ore * 60 + Orario.Minuti
Secondi = Minuti * 60 + Orario.Secondi
return Secondi
Tutto quello che ci serve è ora un modo per convertire da un intero ad un oggetto Tempo:
def ConverteInTempo(Secondi):
Orario = Tempo()
Orario.Ore = Secondi/3600
Secondi = Secondi - Orario.Ore * 3600
Orario.Minuti = Secondi/60
Secondi = Secondi - Orario.Minuti * 60
Orario.Secondi = Secondi
return Orario
Forse dovrai pensarci un po' su per convincerti che questa tecnica per convertire un numero da una base all'altra è formalmente corretta. Comunque ora puoi usare queste funzioni per riscrivere SommaTempi:
def SommaTempi(T1, T2):
Secondi = ConverteInSecondi(T1) + ConverteInSecondi(T2)
return ConverteInTempo(Secondi)
Questa versione è molto più concisa dell'originale e ed è molto più facile dimostrare la sua correttezza.
Esercizio: riscrivi Incremento usando lo stesso principio.
Sicuramente la conversione numerica da base 10 a base 60 e viceversa è meno intuitiva da capire, data la sua astrazione. Il nostro intuito ci aveva portato a lavorare con i tempi in un modo molto più comprensibile anche se meno efficace.
Malgrado lo sforzo iniziale abbiamo progettato il nostro programma facendo in modo di trattare i tempi come numeri in base 60, il tempo investito nello scrivere le funzioni di conversione viene abbondantemente recuperato quando riusciamo a scrivere un programma molto più corto, facile da leggere e correggere, e soprattutto più affidabile.
Se il programma è progettato in modo oculato è anche più facile aggiungere nuove caratteristiche. Per esempio immagina di sottrarre due tempi per determinare l'intervallo trascorso. L'approccio iniziale avrebbe portato alla necessità di dover implementare una sottrazione con il prestito. Con le funzioni di conversione, scritte una sola volta ma riutilizzate in varie funzioni, è molto più facile e rapido avere un programma funzionante anche in questo caso.
Talvolta il fatto di rendere un problema più generale e quindi leggermente più difficile da implementare permette di gestirlo in modo più semplice dato che ci sono meno casi speciali da gestire e di conseguenza minori possibilità di errore.
Quando trovi una soluzione ad una classe di problemi, invece che ad un singolo problema, hai a che fare con un algoritmo. Abbiamo già usato questa parola in precedenza ma non l'abbiamo propriamente definita ed il motivo risiede nel fatto che non è facile trovare una definizione. Proveremo un paio di approcci.
Consideriamo qualcosa che non è un algoritmo: quando hai imparato a moltiplicare due numeri di una cifra hai sicuramente memorizzato la tabella della moltiplicazione. In effetti si è trattato di memorizzare 100 soluzioni specifiche: questo tipo di lavoro non è un algoritmo.
Ma se sei stato "pigro" probabilmente hai trovato delle scorciatoie che ti hanno permesso di alleggerire il lavoro. Per fare un esempio per moltiplicare n per 9 potevi scrivere n-1 come prima cifra, seguito da 10-n come seconda cifra. Questo sistema è una soluzione generale per moltiplicare ogni numero di una cifra maggiore di zero per 9: in questo caso ci troviamo a che fare con un algoritmo.
Le varie tecniche che hai imparato per calcolare la somma col riporto, la sottrazione con il prestito, la moltiplicazione, la divisione sono tutti algoritmi. Una delle caratteristiche degli algoritmi è che non richiedono intelligenza per essere eseguiti in quanto sono processi meccanici nei quali ogni passo segue il precedente secondo un insieme di regole più o meno semplice.
D'altro canto la progettazione di algoritmi è molto interessante ed intellettualmente stimolante rappresentando il fulcro di ciò che chiamiamo programmazione.
Alcune delle cose più semplici che facciamo naturalmente, senza difficoltà o pensiero cosciente, sono tra le cose più difficili da esprimere sotto forma di algoritmo. Comprendere un linguaggio naturale è un buon esempio: lo sappiamo fare tutti ma finora nessuno è stato in grado di spiegare come ci riusciamo esprimendolo sotto forma di algoritmo.