Next Up Previous Hi Index

Chapter 5

Funzioni produttive

5.1 Valori di ritorno

Alcune delle funzioni predefinite che abbiamo usato finora producono dei risultati: la chiamata della funzione con un particolare argomento genera un nuovo valore che viene in seguito assegnato ad una variabile o viene usato come parte di un'espressione.

e = math.exp(1.0)
Altezza = Raggio * math.sin(Angolo)

Nessuna delle funzioni che abbiamo scritto sino a questo momento ha ritornato un valore.

In questo capitolo scriveremo funzioni che ritornano un valore e che chiamiamo funzioni produttive. \ Il primo esempio è AreaDelCerchio che ritorna l'area di un cerchio per un dato raggio:

import math

def AreaDelCerchio(Raggio):
  temp = math.pi * Raggio**2
  return temp

Abbiamo già visto l'istruzione return, ma nel caso di una funzione produttiva questa istruzione prevede un valore di ritorno. Questa istruzione significa: "ritorna immediatamente da questa funzione a quella chiamante e usa questa espressione come valore di ritorno". L'espressione che rappresenta il valore di ritorno può essere anche complessa, così che l'esempio visto in precedenza può essere riscritto in modo più conciso:

def AreaDelCerchio(raggio):
  return math.pi * Raggio**2

D'altra parte una variabile temporanea come temp spesso rende il programma più leggibile e ne semplifica il debug.

Talvolta è necessario prevedere delle istruzioni di ritorno multiple, ciascuna all'interno di una ramificazione di un'istruzione condizionale:

def ValoreAssoluto(x):
  if x < 0:
    return -x
  else:
    return x

Dato che queste istruzioni return sono in rami diversi della condizione solo una di esse verrà effettivamente eseguita.

Il codice che è posto dopo un'istruzione return, o in ognuno dei posti dove non può essere raggiunto dal flusso di esecuzione, è denominato codice morto.

In una funzione produttiva è una buona idea assicurarci che ognuna delle ramificazioni possibili porti ad un'uscita dalla funzione con un'istruzione di return. Per esempio:

def ValoreAssoluto(x):
  if x < 0:
    return -x
  elif x > 0:
    return x

Questo programma non è corretto in quanto non è prevista un'uscita con return nel caso x sia 0. In questo caso il valore di ritorno è un valore speciale chiamato None:

>>> print ValoreAssoluto(0)
None

Esercizio: scrivi una funzione Confronto che ritorna 1 se x>y, 0 se x==y e -1 se x<y.

5.2 Sviluppo del programma

A questo punto sei già in grado di leggere funzioni complete e capire cosa fanno. Inoltre se hai fatto gli esercizi che ti ho suggerito hai già scritto qualche piccola funzione. A mano a mano che scriverai funzioni di complessità maggiore comincerai ad incontrare qualche difficoltà soprattutto con gli errori di semantica e di esecuzione.

Per fare fronte a questi programmi via via più complessi ti suggerisco una tecnica chiamata sviluppo incrementale. Lo scopo dello sviluppo incrementale è evitare lunghe sessioni di debug, aggiungendo e testando continuamente piccole parti di codice alla volta.

Come programma di esempio supponiamo che tu voglia trovare la distanza tra due punti conoscendone le coordinate (x1, y1) e (x2, y2). Con il teorema di Pitagora sappiamo che la distanza è

distanza =

(x2 - x1)2 + (y2 - y1)2

La prima cosa da considerare è l'aspetto che la funzione DistanzaTraDuePunti deve avere in Python chiarendo subito quali siano i parametri che si vogliono passare alla funzione e quale sia il risultato da ottenere: quest'ultimo può essere tanto un valore numerico da utilizzare all'interno di una espressione o da assegnare ad una variabile, tanto una stampa a video o altro.

Nel nostro caso è chiaro che le coordinate dei due punti sono i nostri parametri, e la distanza calcolata un valore numerico in virgola mobile.

Possiamo così delineare un primo abbozzo di funzione:

def DistanzaTraDuePunti(x1, y1, x2, y2):
  return 0.0

Ovviamente questa prima versione non calcola distanze, in quanto ritorna sempre 0. Ma è già una funzione sintatticamente corretta e può essere eseguita: è il caso di eseguire questo primo test prima di procedere a renderla più complessa.

Per testare la nuova funzione proviamo a chiamarla con dei semplici valori:

>>> DistanzaTraDuePunti(1, 2, 4, 6)
0.0

Abbiamo scelto questi valori così che la loro distanza orizzontale è 3 e quella verticale è 4. Con il teorema di Pitagora è facile vedere che il valore atteso è pari a 5 (5 è la lunghezza dell'ipotenusa di un triangolo rettangolo i cui cateti sono 3 e 4). Quando testiamo una funzione è sempre utile conoscere il risultato di qualche caso particolare per verificare se stiamo procedendo sulla strada giusta.

A questo punto abbiamo verificato che la funzione è sintatticamente corretta e possiamo così cominciare ad aggiungere linee di codice. Dopo ogni aggiunta la testiamo ancora per vedere che non ci siano problemi evidenti. Dovesse presentarsi un problema almeno sapremo che questo è dovuto alle linee inserite dopo l'ultimo test che ha avuto successo.

Un passo logico per risolvere il nostro problema è quello di trovare le differenze x2-x1 e y2-y1. Memorizzeremo queste differenze in variabili temporanee chiamate dx e dy e le stamperemo a video.

def DistanzaTraDuePunti(x1, y1, x2, y2):
  dx = x2 - x1
  dy = y2 - y1
  print "dx vale", dx
  print "dy vale", dy
  return 0.0

Se la funzione lavora correttamente, quando la richiamiamo con i valori di prima dovremmo trovare che dx e dy valgono rispettivamente 3 e 4. Se i risultati coincidono siamo sicuri che la funzione carica correttamente i parametri ed elabora altrettanto correttamente le prime righe. Nel caso il risultato non fosse quello atteso, dovremo concentrarci solo sulle poche righe aggiunte dall'ultimo test e non sull'intera funzione.

Proseguiamo con il calcolo della somma dei quadrati di dx e dy:

def DistanzaTraDuePunti(x1, y1, x2, y2):
  dx = x2 - x1
  dy = y2 - y1
  DistQuadrata = dx**2 + dy**2
  print "DistQuadrata vale ", DistQuadrata
  return 0.0

Nota come i due print che avevamo usato prima siano stati rimossi in quanto ci sono serviti per testare quella parte di programma ma adesso sarebbero inutili. Un codice come questo è chiamato codice temporaneo perché è utile durante la costruzione del programma ma alla fine deve essere rimosso in quanto non fa parte delle funzioni richieste alla versione definitiva della nostra funzione.

Ancora una volta eseguiamo il programma. Se tutto funziona dovremmo trovare un risultato pari a 25 (la somma dei quadrati costruiti sui cateti di lato 3 e 4).

Non ci resta che calcolare la radice quadrata. Se abbiamo importato il modulo matematico math possiamo usare la funzione sqrt per elaborare il risultato:

def DistanzaTraDuePunti(x1, y1, x2, y2):
  dx = x2 - x1
  dy = y2 - y1
  DistQuadrata = dx**2 + dy**2
  Risultato = math.sqrt(DistQuadrata)
  return Risultato

Stavolta se tutto va bene abbiamo finito. Potresti anche stampare il valore di Risultato prima di uscire dalla funzione con return.

Soprattutto all'inizio non dovresti mai aggiungere più di poche righe di programma alla volta. Man mano che la tua esperienza di programmatore cresce ti troverai a scrivere pezzi di codice sempre più grandi. In ogni caso nelle prime fasi il processo di sviluppo incrementale ti farà risparmiare un bel po' di tempo.

Ecco gli aspetti chiave del processo di sviluppo incrementale:

  1. Inizia con un programma funzionante e fai piccoli cambiamenti: questo ti permetterà di scoprire facilmente dove siano localizzati gli eventuali errori.
  2. Usa variabili temporanee per memorizzare i valori intermedi, così da poterli stampare e controllare.
  3. Quando il programma funziona perfettamente rimuovi le istruzioni temporanee e consolida le istruzioni in espressioni composite, sempre che questo non renda il programma difficile da leggere.
Esercizio: usa lo sviluppo incrementale per scrivere una funzione chiamata Ipotenusa che ritorna la lunghezza dell'ipotenusa di un triangolo rettangolo, passando i due cateti come parametri. Registra ogni passo del processo di sviluppo man mano che esso procede.

5.3 Composizione

È possibile chiamare una funzione dall'interno di un'altra funzione. Questa capacità è chiamata composizione.

Scriveremo ora una funzione che accetta come parametri il centro ed un punto sulla circonferenza di un cerchio e calcola l'area del cerchio.

Il centro del cerchio è memorizzato nelle variabili xc e yc e le coordinate del punto sulla circonferenza in xp e yp. Il primo passo è trovare il raggio del cerchio, che è equivalente alla distanza tra i due punti: la funzione DistanzaTraDuePunti che abbiamo appena scritto servirà proprio a questo:

Raggio = DistanzaTraDuePunti(xc, yc, xp, yp)

Il secondo passo è trovare l'area del cerchio e restituirla:

Risultato = AreaDelCerchio(Raggio)
return Risultato

Assemblando il tutto in una funzione abbiamo:

def AreaDelCerchio2(xc, yc, xp, yp):
  Raggio = DistanzaTraDuePunti(xc, yc, xp, yp)
  Risultato = AreaDelCerchio(Raggio)
  return Risultato

Abbiamo chiamato questa funzione AreaDelCerchio2 per distinguerla dalla funzione AreaDelCerchio definita in precedenza. Non possono esistere due funzioni con lo stesso nome all'interno di un modulo.

Le variabili temporanee Raggio e Risultato sono utili per lo sviluppo e il debug ma quando il programma funziona possiamo riscrivere la funzione in modo più conciso componendo le chiamate alle funzioni:

def AreaDelCerchio2(xc, yc, xp, yp):
  return AreaDelCerchio(DistanzaTraDuePunti(xc, yc, xp, yp))

Esercizio: scrivi una funzione Pendenza(x1, y1, x2, y2) che ritorna il valore della pendenza della retta passante per i punti (x1, y1) e (x2, y2). Poi usa questa funzione in una seconda funzione chiamata IntercettaY(x1, y1, x2, y2) che ritorna il valore delle ordinate quando la retta determinata dagli stessi punti ha X uguale a zero.

5.4 Funzioni booleane

Le funzioni possono anche ritornare valori booleani (vero o falso) e questo è molto utile per mascherare al loro interno test anche complicati.

def Divisibile(x, y):
  if x % y == 0:
    return 1       # x e' divisibile per y: ritorna vero
  else:
    return 0       # x non e' divisibile per y: ritorna falso

Il nome di questa funzione è Divisibile (sarebbe comodo poterla chiamare E`Divisibile ma purtroppo gli accenti e le lettere accentate non sono caratteri validi nei nomi di variabili e di funzioni). È consuetudine assegnare dei nomi che sembrano domande con risposta si/no alle funzioni booleane: Divisibile? Bisestile? NumeroPari? Nel nostro caso Divisibile ritorna 1 o 0 per indicare se x è divisibile o meno per y. Vale il discorso già fatto in precedenza: 0 indica falso, qualsiasi valore diverso da 0 vero.

Possiamo rendere le funzioni ancora più concise avvantaggiandoci del fatto che la condizione nell'istruzione if è anch'essa di tipo booleano:

def Divisibile(x, y):
  return x%y == 0

Questa sessione mostra la nuova funzione in azione:

>>>   Divisibile(6, 4)
0
>>>   Divisibile(6, 3)
1

Le funzioni booleane sono spesso usate in istruzioni condizionali:

if Divisibile(x, y):
  print x, "e' divisibile per", y
else:
  print x, "non e' divisibile per", y

Esercizio: scrivi una funzione CompresoTra(x,y,z) che ritorna 1 se yle xle z, altrimenti ritorna 0.

5.5 Ancora ricorsione

Finora hai imparato una piccola parte di Python, ma potrebbe interessarti sapere che questo sottoinsieme è già di per sé un linguaggio di programmazione completo: questo significa che con gli elementi che già conosci puoi esprimere qualsiasi tipo di elaborazione. Aggiungendo solo qualche comando di controllo per gestire tastiera, mouse, dischi, ecc. qualsiasi tipo di programma potrebbe già essere riscritto usando solo le caratteristiche del linguaggio che hai imparato finora.

La prova di questa affermazione è un esercizio non banale e fu dimostrata per la prima volta da Alan Turing, uno dei primi teorici dell'informatica (qualcuno potrebbe obiettare che in realtà era un matematico, ma molti degli informatici di allora erano dei matematici). Di conseguenza la dimostrazione è chiamata Teorema di Turing.

Per darti un'idea di che cosa puoi fare con ciò che hai imparato finora proveremo a valutare un po' di funzioni matematiche definite ricorsivamente. Una funzione ricorsiva è simile ad una definizione circolare, nel senso che la sua definizione contiene un riferimento alla cosa che viene definita. Una definizione circolare non è poi troppo utile, tanto che se ne trovassi una consultando un vocabolario ciò ti darebbe fastidio:

zurloso
aggettivo usato per descrivere qualcosa di zurloso.

D'altra parte se guardi la definizione della funzione matematica fattoriale (indicata da un numero seguito da un punto esclamativo) ti accorgi che la somiglianza è notevole:

0! = 1
n! = n (n-1)!

Questa definizione stabilisce che il fattoriale di 0 è 1 e che il fattoriale di ogni altro valore n è n moltiplicato per il fattoriale di n-1.

Così 3! è 3 moltiplicato 2!, che a sua volta è 2 moltiplicato 1!, che a sua volta è 1 moltiplicato 0!, che per definizione è 1. Mettendo tutto assieme 3! è uguale a 3 per 2 per 1, e cioè pari a 6.

Se scrivi una definizione ricorsiva, solitamente puoi anche scrivere un programma Python per valutarla. Il primo passo è quello di decidere quali siano i parametri da passare alla funzione.

Fattoriale ha un solo parametro:

def Fattoriale(n):

Se l'argomento è 0 dobbiamo ritornare il valore 1:

def Fattoriale(n):
  if n == 0:
    return 1

Altrimenti, e questa è la parte interessante, dobbiamo fare una chiamata ricorsiva per trovare il fattoriale di n-1 e poi moltiplicare questo valore per n:

def Fattoriale(n):
  if n == 0:
    return 1
  else:
    FattorialeMenoUno = Fattoriale(n-1)
    Risultato = n * FattorialeMenoUno
    return Risultato

Il flusso di esecuzione del programma è simile a quello di ContoAllaRovescia nella sezione 4.9. Se chiamiamo Fattoriale con il valore 3:

Dato che 3 non è 0, seguiamo il ramo else e calcoliamo il fattoriale di n=3-1=2...

Dato che 2 non è 0, seguiamo il ramo else e calcoliamo il fattoriale di n=2-1=1...
Dato che 1 non è 0, seguiamo il ramo else e calcoliamo il fattoriale di n=1-1=0...
Dato che 0 è 0 ritorniamo 1 senza effettuare ulteriori chiamate ricorsive.

Il valore di ritorno (1) è moltiplicato per n (1) e il risultato (1) restituito alla funzione chiamante.

Il valore di ritorno (1) è moltiplicato per n (2) e il risultato (2) restituito alla funzione chiamante.

Il valore di ritorno (2) è moltiplicato per n (3) e il risultato (6) diventa il valore di ritorno della funzione che ha fatto partire l'intero processo.

Questo è il diagramma di stack per l'intera serie di funzioni:

I valori di ritorno sono mostrati mentre vengono passati di chiamata in chiamata verso l'alto. In ogni frame il valore di ritorno è Risultato, che è il prodotto di n per FattorialeMenoUno.

Nota come nell'ultimo frame le variabili locali FattorialeMenoUno e Risultato non esistono, perché il ramo che le crea non viene eseguito.

5.6 Accettare con fiducia

Seguire il flusso di esecuzione è un modo di leggere i programmi, ma può dimostrarsi piuttosto difficile da seguire man mano che le dimensioni del codice aumentano. Un modo alternativo è ciò che potremmo chiamare accettazione con fiducia: quando arrivi ad una chiamata di funzione invece di seguire il flusso di esecuzione parti dal presupposto che la funzione chiamata si comporti correttamente e che ritorni il valore che ci si attende.

In ogni modo stai già praticando questa accettazione con fiducia quando usi le funzioni predefinite: quando chiami math.cos o math.exp non vai a controllare l'implementazione delle funzioni, assumendo che chi le ha scritte fosse un buon programmatore e che le funzioni siano corrette.

Lo stesso si può dire per le funzioni che scrivi tu stesso: quando abbiamo scritto la funzione Divisibile, che controlla se un numero è divisibile per un altro, e abbiamo verificato che la funzione è corretta controllando il codice possiamo usarla senza doverla ricontrollare ancora.

Quando hai chiamate ricorsive invece di seguire il flusso di programma puoi partire dal presupposto che la chiamata ricorsiva funzioni (producendo il risultato corretto) chiedendoti in seguito: "Supponendo che si riesca a trovare il fattoriale di n-1, posso calcolare il fattoriale di n?" In questo caso è chiaro che puoi farlo moltiplicandolo per n. È certamente strano partire dal presupposto che una funzione lavori correttamente quando non è ancora stata finita, non è vero?

5.7 Un esempio ulteriore

Nell'esempio precedente abbiamo usato delle variabili temporanee per identificare meglio i singoli passaggi e per facilitare la lettura del codice, ma avremmo potuto risparmiare qualche riga:

def Fattoriale(n):
  if n == 0:
    return 1
  else:
    return n * Fattoriale(n-1)

D'ora in poi in questo libro tenderemo ad usare la forma più concisa, ma ti consiglio di usare quella più esplicita finché non avrai un po' di esperienza nello sviluppo del codice.

Dopo il Fattoriale, l'esempio di funzione ricorsiva più comune è la funzione Fibonacci che ha questa definizione:

fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2);

Tradotta in Python:

def Fibonacci (n):
  if n == 0 or n == 1:
    return 1
  else:
    return Fibonacci(n-1) + Fibonacci(n-2)

Con una funzione del genere il flusso di esecuzione diventa praticamente impossibile da seguire anche per piccoli valori di n. In questo caso ed in casi analoghi vale la pena di adottare l'accettazione con fiducia partendo dal presupposto che le due chiamate ricorsive funzionino correttamente e che quindi la somma dei loro valori di ritorno sia corretta.

5.8 Controllo dei tipi

Cosa succede se chiamiamo Fattoriale e passiamo 1.5 come argomento?

>>> Fattoriale(1.5)
RuntimeError: Maximum recursion depth exceeded

A prima vista sembra una ricorsione infinita. Ma come può accadere? C'è un caso base (quando n==0) che dovrebbe fermare la ricorsione, ma il problema è che non tutti i possibili valori di n verificano la condizione di fermata prevista dal caso base.

Se proviamo a seguire il flusso di esecuzione, alla prima chiamata il valore di n passa a 0.5. Alla successiva diventa -0.5. Da lì in poi, sottraendo 1 di volta in volta, il valore passato alla funzione è sempre più piccolo ma non sarà mai lo 0 che ci aspettiamo nel caso base.

Abbiamo due scelte: possiamo generalizzare la funzione Fattoriale per farla lavorare anche nel caso di numeri in virgola mobile, o possiamo far controllare alla funzione dopo la sua chiamata se il parametro passato è del tipo corretto. La prima possibilità è chiamata in matematica funzione gamma (il fattoriale definito nei numeri reali) ed è decisamente al di là degli scopi di questo libro, così sceglieremo la seconda alternativa.

Possiamo usare type per controllare se il parametro è di tipo intero. Già che ci siamo mettiamo anche un controllo per essere sicuri che il numero sia positivo:

def Fattoriale(n):
  if type(n) != type(1):
    print "Il fattoriale è definito solo per i valori interi."
    return -1
  elif n < 0:
    print "Il fattoriale è definito solo per interi positivi."
    return -1
  elif n == 0:
    return 1
  else:
    return n * Fattoriale(n-1)

Nel primo confronto abbiamo confrontato il "tipo di n" con il "tipo del numero intero 1" per vedere se n è intero.

Ora abbiamo tre casi: il primo blocca i valori non interi; il secondo gli interi negativi ed il terzo calcola il fattoriale di un numero che a questo punto è sicuramente un intero positivo o uguale a zero. Nei primi due casi dato che il calcolo non è possibile viene stampato un messaggio d'errore e la funzione ritorna il valore -1, per indicare che qualcosa non ha funzionato:

>>> Fattoriale("AAA")
Il fattoriale è definito solo per i valori interi.
-1
>>> Fattoriale (-2)
Il fattoriale è definito solo per gli interi positivi.
-1

Se il flusso di programma passa attraverso entrambi i controlli siamo certi che n è un intero positivo e sappiamo che la ricorsione avrà termine.

Questo programma mostra il funzionamento di una condizione di guardia. I primi due controlli agiscono da "guardiani", proteggendo il codice che segue da circostanze che potrebbero causare errori. Le condizioni di guardia rendono possibile provare la correttezza del codice in modo estremamente semplice ed affidabile.

5.9 Glossario

Funzione produttiva
funzione che produce un valore.
Valore di ritorno
valore restituito da una funzione.
Variabile temporanea
variabile usata per memorizzare un risultato intermedio durante un calcolo complesso.
Codice morto
parte di un programma che non può mai essere eseguita, spesso perché compare dopo un'istruzione di return.
Valore None
valore speciale ritornato da una funzione che non ha un'istruzione return, o se l'istruzione return non specifica un valore di ritorno.
Sviluppo incrementale
sistema di sviluppo del programma inteso ad evitare lunghe sessioni di debug alla ricerca degli errori aggiungendo e testando solo piccole porzioni di codice alla volta.
Codice temporaneo
codice inserito solo nella fase di sviluppo del programma e che non è richiesto nella versione finale.
Condizione di guardia
condizione che controlla e gestisce le circostanze che possono causare un errore.


Next Up Previous Hi Index