Next Up Previous Hi Index

Chapter 6

Iterazione

6.1 Assegnazione e confronto

Come puoi avere già scoperto è possibile assegnare più valori ad una stessa variabile, con la variabile che assume sempre l'ultimo valore assegnato:

Numero = 5
print Numero,
Numero = 7
print Numero

La stampa di questo programma è 5 7, perché la prima volta che Numero è stampato il suo valore è 5, la seconda 7. La virgola dopo la prima istruzione print evita il ritorno a capo dopo la stampa così che entrambi i valori appaiono sulla stessa riga.

Questo è il diagramma di stato per quest'assegnazione:

Nel caso di assegnazioni ripetute è particolarmente importante distinguere tra operazioni di assegnazione e controlli di uguaglianza. Python usa (=) per l'assegnazione e si potrebbe essere tentati di interpretare l'istruzione a = b come un controllo di equivalenza, ma non lo è!

In primo luogo l'equivalenza è commutativa mentre l'assegnazione non lo è: in matematica se a = 7 allora 7 = a; in Python l'istruzione a=7 è legale mentre 7=a produce un errore di sintassi.

Inoltre in matematica un'uguaglianza è sempre vera: se a = b, a sarà sempre uguale a b. In Python un'assegnazione può rendere due variabili uguali ma raramente l'uguaglianza sarà mantenuta a lungo:

a = 5
b = a    # a e b sono uguali
a = 3    # ora a e b sono diversi

La terza riga cambia il valore di a ma non cambia il valore di b. In qualche linguaggio di programmazione sono usati simboli diversi per l'assegnazione, tipo <- o :=, per evitare ogni malinteso.

6.2 L'istruzione while

I computer sono spesso usati per automatizzare compiti ripetitivi: il noiosissimo compito di ripetere operazioni identiche o simili un gran numero di volte senza fare errori è qualcosa che riesce bene ai computer.

Abbiamo visto due programmi, NRigheVuote e ContoAllaRovescia, che usano la ricorsione per eseguire una ripetizione. Questa ripetizione è più comunemente chiamata iterazione. Dato che l'iterazione è così comune, Python fornisce vari sistemi per renderla più semplice da implementare. Il primo sistema è l'istruzione while.

Ecco come ContoAllaRovescia viene riscritto usando l'istruzione while:

def ContoAllaRovescia(n):
  while n > 0:
    print n
    n = n-1
  print "Partenza!"

La chiamata ricorsiva è stata rimossa e quindi questa funzione ora non è più ricorsiva.

Puoi leggere il programma con l'istruzione while come fosse scritto in un linguaggio naturale: "Finché (while) n è più grande di 0 stampa il valore di n e poi diminuiscilo di 1. Quando arrivi a 0 stampa la stringa Partenza!".

In modo più formale ecco il flusso di esecuzione di un'istruzione while:

  1. Valuta la condizione controllando se essa è vera (1) o falsa (0).
  2. Se la condizione è falsa esci dal ciclo while e continua l'esecuzione dalla prima istruzione che lo segue.
  3. Se la condizione è vera esegui tutte le istruzioni nel corpo del while e torna al passo 1.

Il corpo del ciclo while consiste di tutte le istruzioni che seguono l'intestazione e che hanno la stessa indentazione.

Questo tipo di flusso è chiamato ciclo o loop. Nota che se la condizione è falsa al primo controllo, le istruzioni del corpo non sono mai eseguite.

Il corpo del ciclo dovrebbe cambiare il valore di una o più variabili così che la condizione possa prima o poi diventare falsa e far così terminare il ciclo. In caso contrario il ciclo si ripeterebbe all'infinito, determinando un ciclo infinito.

Nel caso di ContoAllaRovescia possiamo essere certi che il ciclo è destinato a terminare visto che n è finito ed il suo valore diventa via via più piccolo così da diventare, prima o poi, pari a zero. In altri casi può non essere così facile stabilire se un ciclo avrà termine:

def Sequenza(n):
  while n != 1:
    print n,
    if n%2 == 0:        # se n e' pari
      n = n/2
    else:               # se n e' dispari
      n = n*3+1

La condizione per questo ciclo è n!=1 cosicché il ciclo si ripeterà finché n è diverso da 1.

Ogni volta che viene eseguito il ciclo il programma stampa il valore di n e poi controlla se è pari o dispari. Se è pari, n viene diviso per 2. Se dispari, è moltiplicato per 3 e gli viene sommato 1. Se il valore passato è 3, la sequenza risultante è 3, 10, 5, 16, 8, 4, 2, 1.

Dato che n a volte sale e a volte scende in modo abbastanza casuale non c'è una prova ovvia che n raggiungerà 1 in modo da far terminare il ciclo. Per qualche particolare valore di n possiamo facilmente determinare a priori il suo termine (per esempio per le potenze di 2) ma per gli altri nessuno è mai riuscito a trovare la dimostrazione che il ciclo ha termine.

Esercizio: riscrivi la funzione NRigheVuote della sezione 4.9 usando un'iterazione invece che la ricorsione.

6.3 Tabelle

Una delle cose per cui sono particolarmente indicati i cicli è la generazione di tabulati. Prima che i computer fossero comunemente disponibili si dovevano calcolare a mano logaritmi, seni, coseni e i valori di tante altre funzioni matematiche. Per rendere più facile il compito i libri di matematica contenevano lunghe tabelle di valori la cui stesura comportava enormi quantità di lavoro molto noioso e grosse possibilità di errore.

Quando apparvero i computer l'idea iniziale fu quella di usarli per generare tabelle prive di errori. La cosa che non si riuscì a prevedere fu il fatto che i computer sarebbero diventati così diffusi e disponibili a tutti da rendere quei lunghi tabulati cartacei del tutto inutili. Per alcune operazioni i computer usano ancora tabelle simili in modo del tutto nascosto dall'operatore: vengono usate per ottenere risposte approssimate che poi vengono rifinite per migliorarne la precisione. In qualche caso ci sono stati degli errori in queste tabelle "interne", il più famoso dei quali ha avuto come protagonista il Pentium Intel con un errore nel calcolo delle divisioni in virgola mobile.

Sebbene la tabella dei logaritmi non sia più utile come lo era in passato rimane tuttavia un buon esempio di iterazione. Il programma seguente stampa una sequenza di valori nella colonna di sinistra e il loro logaritmo in quella di destra:

x = 1.0
while x < 10.0:
  print x, '\t', math.log(x)
  x = x + 1.0

La stringa '\t' rappresenta un carattere di tabulazione.

A mano a mano che caratteri e stringhe sono mostrati sullo schermo un marcatore invisibile chiamato cursore tiene traccia di dove andrà stampato il carattere successivo. Dopo un'istruzione print il cursore normalmente si posiziona all'inizio della riga successiva.

Il carattere di tabulazione sposta il cursore a destra finché quest'ultimo raggiunge una delle posizione di stop delle tabulazioni. Queste posizioni si ripetono a distanze regolari, tipicamente ogni 4 o 8 caratteri. Le tabulazioni sono utili per allineare in modo semplice le colonne di testo. Ecco il prodotto del programma appena visto:

1.0     0.0
2.0     0.69314718056
3.0     1.09861228867
4.0     1.38629436112
5.0     1.60943791243
6.0     1.79175946923
7.0     1.94591014906
8.0     2.07944154168
9.0     2.19722457734

Se questi valori sembrano strani ricorda che la funzione log usa il logaritmo dei numeri naturali e. Dato che le potenze di due sono così importanti in informatica possiamo avere la necessità di calcolare il logaritmo in base 2. Per farlo usiamo questa formula:

log2 x =
loge x

loge 2

Modificando una sola riga di programma:

   print x, '\t',  math.log(x)/math.log(2.0)

otteniamo:

1.0     0.0
2.0     1.0
3.0     1.58496250072
4.0     2.0
5.0     2.32192809489
6.0     2.58496250072
7.0     2.80735492206
8.0     3.0
9.0     3.16992500144

Possiamo vedere che 1, 2, 4 e 8 sono potenze di due perché i loro logaritmi in base 2 sono numeri interi.

Per continuare con le modifiche, invece di sommare qualcosa a x ad ogni ciclo e ottenere così una serie aritmetica, possiamo moltiplicare x per qualcosa ottenendo una serie geometrica. Se vogliamo trovare il logaritmo di altre potenze di due possiamo modificare ancora il programma:

x = 1.0
while x < 100.0:
  print x, '\t', math.log(x)/math.log(2.0)
  x = x * 2.0

Il risultato in questo caso è:

1.0     0.0
2.0     1.0
4.0     2.0
8.0     3.0
16.0    4.0
32.0    5.0
64.0    6.0

Il carattere di tabulazione fa in modo che la posizione della seconda colonna non dipenda dal numero di cifre del valore nella prima.

Anche se i logaritmi possono non essere più così utili per un informatico, conoscere le potenze di due è fondamentale.

Esercizio: modifica questo programma per fare in modo che esso produca le potenze di due fino a 65536 (cioè 216). Poi stampale e imparale a memoria!

Il carattere di backslash '\' in '\t' indica l'inizio di una sequenza di escape. Le sequenze di escape sono usate per rappresentare caratteri invisibili come la tabulazione ('\t') e il ritorno a capo ('\n'). Può comparire in qualsiasi punto di una stringa: nell'esempio appena visto la tabulazione è l'unica cosa presente nella stringa del print.

Secondo te, com'è possibile rappresentare un carattere di backslash in una stringa?

Esercizio: scrivi una stringa singola che quando stampata

produca
        questo
                risultato.

6.4 Tabelle bidimensionali

Una tabella bidimensionale è una tabella dove leggi un valore all'intersezione tra una riga ed una colonna, la tabella della moltiplicazione ne è un buon esempio. Immaginiamo che tu voglia stampare la tabella della moltiplicazione per i numeri da 1 a 6.

Un buon modo per iniziare è scrivere un ciclo che stampa i multipli di 2 tutti su di una stessa riga:

i = 1
while i <= 6:
  print 2*i, '   ',
  i = i + 1
print

La prima riga inizializza una variabile chiamata i che agisce come contatore o indice del ciclo. Man mano che il ciclo viene eseguito i passa da 1 a 6. Quando i è 7 la condizione non è più soddisfatta ed il ciclo termina. Ad ogni ciclo viene mostrato il valore di 2*i seguito da tre spazi.

Ancora una volta vediamo come la virgola in print faccia in modo che il cursore rimanga sulla stessa riga evitando un ritorno a capo. Quando il ciclo sui sei valori è stato completato una seconda istruzione print ha lo scopo di portare il cursore a capo su una nuova riga.

Il risultato del programma è:

2      4      6      8      10     12

6.5 Incapsulamento e generalizzazione

L'incapsulamento è il processo di inserire un pezzo di codice all'interno di una funzione così da permetterti di godere dei vantaggi delle funzioni. Hai già visto due esempi di incapsulamento: StampaParita nella sezione 4.5 e Divisibile nella sezione 5.4.

Generalizzare significa prendere qualcosa di specifico per farlo diventare generale: nel nostro caso prendere il programma che calcola i multipli di 2 e fargli calcolare i multipli di un qualsiasi numero intero.

Questa funzione incapsula il ciclo visto in precedenza e lo generalizza per stampare i primi 6 multipli di n:

def StampaMultipli(n):
  i = 1
  while i <= 6:
    print n*i, '\t',
    i = i + 1
  print

Per incapsulare dobbiamo solo aggiungere la prima linea che dichiara il nome della funzione e la lista dei parametri. Per generalizzare dobbiamo sostituire il valore 2 con il parametro n.

Se chiamiamo la funzione con l'argomento 2 otteniamo lo stesso risultato di prima. Con l'argomento 3 il risultato è:

3      6      9      12     15     18

Con l'argomento 4:

4      8      12     16     20     24

Avrai certamente indovinato come stampare la tabella della moltiplicazione. Chiamiamo ripetutamente StampaMultipli con argomenti diversi all'interno di un secondo ciclo:

i = 1
while i <= 6:
  StampaMultipli(i)
  i = i + 1

Nota come siano simili questo ciclo e quello all'interno di StampaMultipli: tutto quello che abbiamo fatto è stato sostituire l'istruzione print con una chiamata di funzione.

Il risultato di questo programma è la tabella della moltiplicazione:

1      2      3      4      5      6
2      4      6      8      10     12
3      6      9      12     15     18
4      8      12     16     20     24
5      10     15     20     25     30
6      12     18     24     30     36

6.6 Ancora incapsulamento

Per provare ancora con l'incapsulamento andiamo a prendere il codice della sezione precedente e inseriamolo in una funzione:

def TabellaMoltiplicazione6x6():
  i = 1
  while i <= 6:
    StampaMultipli(i)
    i = i + 1

Il processo appena illustrato è un piano di sviluppo piuttosto comune: si sviluppa del codice controllando poche righe in ambiente interprete; solo quando queste righe sono perfettamente funzionanti le inseriamo in una funzione.

Questo modo di procedere è particolarmente utile se all'inizio della stesura del tuo programma non sai come lo dividerai in funzioni. Questo tipo di approccio ti permette di progettare il codice mentre procedi con la stesura.

6.7 Variabili locali

Potresti chiederti com'è possibile che si possa usare la stessa variabile i sia in StampaMultipli che in TabellaMoltiplicazione6x6. Non ci sono problemi quando una funzione cambia il valore della variabile?

La risposta è no dato che la variabile i usata in StampaMultipli e la i in TabellaMoltiplicazione6x6 non sono la stessa variabile.

Le variabili create all'interno della definizione di una funzione sono locali e non puoi accedere al valore di una variabile locale al di fuori della funzione che la ospita. Ciò significa che sei libero di avere variabili con lo stesso nome sempre che non si trovino all'interno di una stessa funzione.

Il diagramma di stack per questo programma mostra che le due variabili chiamate i non sono la stessa variabile. Si riferiscono a valori diversi e cambiandone una l'altra resta invariata.

Il valore di i in TabellaMoltiplicazione6x6 va da 1 a 6. Nel diagramma ha valore 3 e al prossimo ciclo varrà 4. Ad ogni ciclo TabellaMoltiplicazione6x6 chiama StampaMultipli con il valore corrente di i come argomento. Quel valore viene assegnato al parametro n.

All'interno di StampaMultipli il valore di i copre l'intervallo che va da 1 a 6. Nel diagramma è 2 e cambiandolo non ci sono effetti collaterali per la variabile i in TabellaMoltiplicazione6x6.

È comune e perfettamente legale avere variabili locali con lo stesso nome. In particolare nomi come i e j sono usati frequentemente come indici per i cicli.

6.8 Ancora generalizzazione

Se vogliamo generalizzare ulteriormente TabellaMoltiplicazione6x6 potremmo estendere il risultato ad una tabella di moltiplicazione di qualsiasi grandezza, e non solo fino al 6x6. A questo punto dobbiamo anche passare un argomento per stabilire la grandezza desiderata:

def TabellaMoltiplicazioneGenerica(Grandezza):
  i = 1
  while i <= Grandezza:
    StampaMultipli(i)
    i = i + 1

Abbiamo sostituito il valore 6 con il parametro Grandezza. Se chiamiamo TabellaMoltiplicazioneGenerica con l'argomento 7 il risultato è:

1      2      3      4      5      6
2      4      6      8      10     12
3      6      9      12     15     18
4      8      12     16     20     24
5      10     15     20     25     30
6      12     18     24     30     36
7      14     21     28     35     42

Il risultato è corretto fatta eccezione per il fatto che sarebbe meglio avere lo stesso numero di righe e di colonne. Per farlo dobbiamo modificare StampaMultipli per specificare quante colonne la tabella debba avere.

Tanto per essere originali chiamiamo anche questo parametro Grandezza, dimostrando ancora una volta che possiamo avere parametri con lo stesso nome all'interno di funzioni diverse. Ecco l'intero programma:

def StampaMultipli(n, Grandezza):
  i = 1
  while i <= Grandezza:
    print n*i, '\t',
    i = i + 1
  print

def
TabellaMoltiplicazioneGenerica(Grandezza):
  i = 1
  while i <= Grandezza:
    StampaMultipli(i, Grandezza)
    i = i + 1

Quando abbiamo aggiunto il nuovo parametro abbiamo cambiato la prima riga della funzione (l'intestazione) ed il posto dove la funzione è chiamata in TabellaMoltiplicazioneGenerica.

Questo programma genera correttamente la tabella 7x7:

1      2      3      4      5      6      7
2      4      6      8      10     12     14
3      6      9      12     15     18     21
4      8      12     16     20     24     28
5      10     15     20     25     30     35
6      12     18     24     30     36     42
7      14     21     28     35     42     49

Quando generalizzi una funzione nel modo più appropriato, spesso ottieni capacità che inizialmente non erano state previste. Per esempio dato che ab = ba, tutti i numeri compresi nella tabella (fatta eccezione per quelli della diagonale) sono presenti due volte. In caso di necessità puoi modificare una linea in TabellaMoltiplicazioneGenerica per stamparne solo metà. Cambia :

    StampaMultipli(i, Grandezza)

in

    StampaMultipli(i, i)

per ottenere

1
2      4
3      6      9
4      8      12     16
5      10     15     20     25
6      12     18     24     30     36
7      14     21     28     35     42     49

Esercizio: il compito consiste nel tracciare l'esecuzione di questa versione TabellaMoltiplicazioneGenerica e cerca di capire come funziona.

6.9 Funzioni

Abbiamo già menzionato i motivi per cui è consigliato l'uso delle funzioni, senza però entrare nel merito. Adesso ti starai chiedendo a che cosa ci stessimo riferendo. Eccone qualcuno:

6.10 Glossario

Assegnazione ripetuta
assegnazione alla stessa variabile di più valori nel corso del programma.
Iterazione
ripetizione di una serie di istruzioni usando una funzione ricorsiva o un ciclo.
Ciclo
istruzione o gruppo di istruzioni che vengono eseguite ripetutamente finché è soddisfatta una condizione.
Ciclo infinito
ciclo nel quale la condizione di terminazione non è mai soddisfatta.
Corpo
gruppo di istruzioni all'interno di un ciclo.
Indice del ciclo
variabile usata nella condizione di terminazione di un ciclo.
Tabulazione
carattere speciale ('\t') che in un'istruzione di stampa sposta il cursore alla prossima posizione di stop nella riga corrente.
Ritorno a capo
carattere speciale ('\n') che in un'istruzione di stampa sposta il cursore all'inizio della prossima riga.
Cursore
marcatore non visibile che tiene traccia di dove andrà stampato il prossimo carattere.
Sequenza di escape
carattere (\\) seguito da uno o più caratteri, usato per designare dei caratteri non stampabili.
Incapsulare
dividere un programma complesso in componenti più semplici, tipo le funzioni, e isolarne i componenti uno dall'altro usando variabili locali.
Generalizzare
sostituire qualcosa di specifico (come un valore costante) con qualcosa di più generale (come un parametro o una variabile).
Piano di sviluppo
processo per lo sviluppo di un programma. In questo capitolo abbiamo mostrato uno stile di sviluppo basato sulla scrittura di un semplice programma capace di svolgere un compito specifico, per poi estenderlo con l'incapsulamento e la generalizzazione.


Next Up Previous Hi Index