Next Up Previous Hi Index

Chapter 9

Tuple

9.1 Mutabilità e tuple

Finora hai visto due tipi composti: le stringhe (sequenze di caratteri) e le liste (sequenze di elementi di tipo qualsiasi). Una delle differenze che abbiamo notato è che le gli elementi di una lista possono essere modificati, mentre non possono essere alterati i caratteri in una stringa: le stringhe sono infatti immutabili mentre le liste sono mutabili.

C'è un altro tipo di dati in Python, simile alla lista eccetto per il fatto che è immutabile: la tupla. La tupla è una lista di valori separati da virgole:

>>> tupla = 'a', 'b', 'c', 'd', 'e'

Sebbene non sia necessario, è convenzione racchiudere le tuple tra parentesi tonde per ragioni di chiarezza:

>>> tupla = ('a', 'b', 'c', 'd', 'e')

Per creare una tupla con un singolo elemento dobbiamo aggiungere la virgola finale dopo l'elemento:

>>> t1 = ('a',)
>>> type(t1)
<type 'tuple'>

Senza la virgola, infatti, Python tratterebbe ('a') come una stringa tra parentesi:

>>> t2 = ('a')
>>> type(t2)
<type 'string'>

Sintassi a parte le operazioni sulle tuple sono identiche a quelle sulle liste. L'operatore indice seleziona un elemento dalla tupla:

>>> tupla = ('a', 'b', 'c', 'd', 'e')
>>> tupla[0]
'a'

e l'operatore porzione seleziona una serie di elementi consecutivi:

>>> tupla[1:3]
('b', 'c')

A differenza delle liste se cerchiamo di modificare gli elementi di una tupla otteniamo un messaggio d'errore:

>>> tupla[0] = 'A'
TypeError: object doesn't support item assignment

Naturalmente anche se non possiamo modificare gli elementi di una tupla possiamo sempre rimpiazzarla con una sua copia modificata:

>>> tupla = ('A',) + tupla[1:]
>>> tupla
('A', 'b', 'c', 'd', 'e')

9.2 Assegnazione di tuple

Di tanto in tanto può essere necessario scambiare i valori di due variabili. Con le istruzioni di assegnazione convenzionali dobbiamo usare una variabile temporanea. Per esempio per scambiare a e b:

>>> temp = a
>>> a = b
>>> b = temp

Questo approccio è poco intuitivo e l'uso dell'assegnazione di tuple lo rende decisamente più comprensibile:

>>> a, b = b, a

La parte sinistra dell'assegnazione è una tupla di variabili; la parte destra una tupla di valori. Ogni valore è assegnato alla rispettiva variabile. Tutte le espressioni sulla destra sono valutate prima delle assegnazioni. Questa caratteristica rende le tuple estremamente versatili.

Ovviamente il numero di variabili sulla sinistra deve corrispondere al numero di valori sulla destra:

>>> a, b, c, d = 1, 2, 3
ValueError: unpack tuple of wrong size

9.3 Tuple come valori di ritorno

Le funzioni possono ritornare tuple. Per fare un esempio possiamo scrivere una funzione che scambia due valori:

def Scambia(x, y):
  return y, x

e in seguito possiamo assegnare il valore di ritorno della funzione ad una tupla di due variabili:

a, b = Scambia(a, b)

In questo caso non c'è una grande utilità nel rendere Scambia una funzione. Anzi occorre stare attenti ad uno dei pericoli insiti nell'incapsulamento di Scambia:

def Scambia(x, y):      # versione non corretta
  x, y = y, x

Se chiamiamo la funzione con:

Scambia(a, b)

apparentemente tutto sembra corretto, ma quando controlliamo i valori di a e b prima e dopo lo "scambio" in realtà ci accorgiamo che questi non sono cambiati. Perché? Perché quando chiamiamo questa funzione non vengono passate le variabili a e b come argomenti, ma i loro valori. Questi valori vengono assegnati a x e y; al termine della funzione, quando x e y vengono rimosse perché variabili locali, qualsiasi valore in esse contenuto viene irrimediabilmente perso.

Questa funzione non produce messaggi d'errore ma ciononostante non fa ciò che noi volevamo farle fare: questo è un esempio di errore di semantica.

Esercizio: disegna il diagramma di stato della funzione Scambia così da capire perché non funziona.

9.4 Numeri casuali

La maggior parte dei programmi fanno la stessa cosa ogni volta che vengono eseguiti e sono detti per questo deterministici. Di solito un programma deterministico è una cosa voluta in quanto a parità di dati in ingresso ci attendiamo lo stesso risultato. Per alcune applicazioni, invece, abbiamo bisogno che l'esecuzione sia imprevedibile: i videogiochi sono un esempio lampante, ma ce ne sono tanti altri.

Creare un programma realmente non deterministico (e quindi imprevedibile) è una cosa piuttosto difficile, ma ci sono dei sistemi per renderlo abbastanza casuale da soddisfare la maggior parte delle esigenze in tal senso. Uno dei sistemi è quello di generare dei numeri casuali ed usarli per determinare i risultati prodotti dal programma. Python fornisce delle funzioni di base che generano numeri pseudocasuali: questi numeri non sono realmente casuali in senso matematico ma per i nostri scopi saranno più che sufficienti.

Il modulo random contiene una funzione chiamata random che restituisce un numero in virgola mobile compreso tra 0.0 (compreso) e 1.0 (escluso). Ad ogni chiamata di random si ottiene il numero seguente di una lunga serie di numeri pseudocasuali. Per vedere un esempio prova ad eseguire questo ciclo:

import random

for i in range(10):
  x = random.random()
  print x

Per generare un numero casuale (lo chiameremo così d'ora in poi, anche se è sottinteso che la casualità ottenuta non è assoluta) compreso tra 0.0 (compreso) ed un limite superiore Limite (escluso) moltiplica x per Limite.

Esercizio: tenta di generare un numero casuale compreso tra il \linebreak LimiteInferiore (compreso) ed il LimiteSuperiore (escluso).
Esercizio addizionale: genera un numero intero compreso tra il \linebreak LimiteInferiore ed il LimiteSuperiore comprendendo entrambi questi limiti.

9.5 Lista di numeri casuali

Proviamo a scrivere un programma che usa i numeri casuali, iniziando con la costruzione di una lista di questi numeri. ListaCasuale prende un parametro intero Lungh e ritorna una lista di questa lunghezza composta di numeri casuali. Iniziamo con una lista di Lungh zeri e sostituiamo in un ciclo un elemento alla volta con un numero casuale:

def ListaCasuale(Lungh):
  s = [0] * Lungh
  for i in range(Lungh):
    s[i] = random.random()
  return s

Testiamo la funzione generando una lista di otto elementi: per poter controllare i programmi è sempre bene partire con insiemi di dati molto piccoli.

>>> ListaCasuale(8)
[0.11421081445000203, 0.38367479346590505, 0.16056841528993915,
0.29204721527340882, 0.75201663462563095, 0.31790165552578986,
0.43858231029411354, 0.27749268689939965]

I numeri casuali generati da random si ritengono distribuiti uniformemente tanto che ogni valore è egualmente probabile.

Se dividiamo la gamma dei valori generati in intervalli della stessa grandezza e contiamo il numero di valori casuali che cadono in ciascun intervallo dovremmo ottenere, approssimativamente, la stessa cifra in ciascuno, sempre che l'esperimento sia effettuato un buon numero di volte.

Possiamo testare questa affermazione scrivendo un programma per dividere la gamma dei valori in intervalli e contare il numero di valori in ciascuno di essi.

9.6 Conteggio

Un buon approccio a questo tipo di problemi è quello di dividere il problema in sottoproblemi e applicare a ciascun sottoproblema uno schema di soluzione già visto in precedenza.

In questo caso vogliamo attraversare una lista di numeri e contare il numero di volte in cui un valore cade in un determinato intervallo. Questo suona familiare: nella sezione 7.8 abbiamo già scritto un programma che attraversa una stringa e conta il numero di volte in cui appare una determinata lettera. Possiamo allora copiare il vecchio programma e adattarlo al problema corrente. Il programma originale era:

Conteggio = 0
for Carattere in Frutto:
  if Carattere == 'a':
    Conteggio = Conteggio + 1
print Conteggio

Il primo passo è quello di sostituire Frutto con Lista e Carattere con Numero. Questo non cambia il programma ma semplicemente lo rende più leggibile.

Il secondo passo è quello di cambiare la condizione dato che siamo interessati a verificare se Numero cade tra LimiteInferiore e LimiteSuperiore.

Conteggio = 0
for Numero in Lista
  if LimiteInferiore < Numero < LimiteSuperiore:
    Conteggio = Conteggio + 1
print Conteggio

L'ultimo passo è quello di incapsulare questo codice in una funzione chiamata NellIntervallo. I parametri della funzione sono la lista da controllare ed i valori LimiteInferiore and LimiteSuperiore:

def NellIntervallo(Lista, LimiteInferiore, LimiteSuperiore):
  Conteggio = 0
  for Numero in Lista:
    if LimiteInferiore < Numero < LimiteSuperiore:
      Conteggio = Conteggio + 1
  return Conteggio

Copiando e modificando un programma esistente siamo stati capaci di scrivere questa funzione velocemente risparmiando un bel po' di tempo di test. Questo tipo di piano di sviluppo è chiamato pattern matching: se devi cercare una soluzione a un problema che hai già risolto riusa una soluzione che avevi già trovato, modificandola per adattarla quel tanto che serve in base alle nuove circostanze.

9.7 Aumentare il numero degli intervalli

A mano a mano che il numero degli intervalli cresce NellIntervallo diventa poco pratica da gestire. Con due soli intervalli ce la caviamo ancora bene:

Intervallo1 = NellIntervallo(a, 0.0, 0.5)
Intervallo2 = NellIntervallo(a, 0.5, 1.0)

ma con quattro intervalli è facile commettere errori sia nel calcolo dei limiti sia nella battitura dei numeri:

Intervallo1 = NellIntervallo(a, 0.0, 0.25)
Intervallo2 = NellIntervallo(a, 0.25, 0.5)
Intervallo3 = NellIntervallo(a, 0.5, 0.75)
Intervallo4 = NellIntervallo(a, 0.75, 1.0)

Ci sono due ordini di problemi: il primo è che dobbiamo creare un nome di variabile per ciascun risultato; il secondo è che dobbiamo calcolare a mano i limiti inferiore e superiore per ciascun intervallo prima di chiamare la funzione.

Risolveremo innanzitutto questo secondo problema: se il numero degli intervalli che vogliamo considerare è NumIntervalli allora l'ampiezza di ogni intervallo è 1.0 / NumIntervalli.

Possiamo usare un ciclo per calcolare i limiti inferiore e superiore per ciascun intervallo, usando i come indice del ciclo da 0 a NumIntervalli-1:

AmpiezzaIntervallo = 1.0 / NumIntervalli
for i in range(NumIntervalli):
  LimiteInferiore = i * AmpiezzaIntervallo
  LimiteSuperiore = LimiteInferiore + AmpiezzaIntervallo
  print "da", LimiteInferiore, "a", LimiteSuperiore

Per calcolare il limite inferiore di ogni intervallo abbiamo moltiplicato l'indice del ciclo per l'ampiezza di ciascun intervallo; per ottenere il limite superiore abbiamo sommato al limite inferiore la stessa ampiezza.

Con NumIntervalli = 8 il risultato è:

da 0.0 a 0.125
da 0.125 a 0.25
da 0.25 a 0.375
da 0.375 a 0.5
da 0.5 a 0.625
da 0.625 a 0.75
da 0.75 a 0.875
da 0.875 a 1.0

Puoi vedere come ogni intervallo sia della stessa ampiezza, come tutta la gamma da 0.0 a 1.0 sia presente e come non ci siano intervalli che si sovrappongono.

Ora torniamo al primo problema: abbiamo bisogno di memorizzare 8 interi senza dover creare variabili distinte. Le liste ci vengono in aiuto e l'indice del ciclo sembra essere un ottimo sistema per selezionare di volta in volta un elemento della lista.

Creiamo la lista dei conteggi all'esterno del ciclo dato che la dobbiamo creare una sola volta (e non ad ogni ciclo). All'interno del ciclo chiameremo ripetutamente NellIntervallo e aggiorneremo l'i-esimo elemento della lista dei conteggi:

NumIntervalli = 8
Conteggio = [0] * NumIntervalli
AmpiezzaIntervallo = 1.0 / NumIntervalli
for i in range(NumIntervalli):
  LimiteInferiore = i * AmpiezzaIntervallo
  LimiteSuperiore = LimiteInferiore + AmpiezzaIntervallo
  Conteggio[i] = NellIntervallo(Lista, LimiteInferiore, \
                 LimiteSuperiore)
print Conteggio

Con una lista di 1000 valori questo programma produce una lista di conteggi di questo tipo:

[138, 124, 128, 118, 130, 117, 114, 131]

Ci aspettavamo per ogni intervallo un valore medio di 125 (1000 numeri divisi per 8 intervalli) ed in effetti ci siamo andati abbastanza vicini da poter affermare che il generatore di numeri casuali si comporta in modo sufficientemente realistico.

Esercizio: prova questa funzione con liste più lunghe per vedere se il conteggio di valori in ogni intervallo tende o meno a livellarsi (maggiore è il numero di prove più i valori dovrebbero diventare simili).

9.8 Una soluzione in una sola passata

Anche se il programma funziona correttamente non è ancora sufficientemente efficiente. Ogni volta che il ciclo chiama NellIntervallo viene attraversata l'intera lista. A mano a mano che il numero di intervalli cresce questo implica un gran numero di attraversamenti di liste.

Sarebbe meglio riuscire a fare un singolo attraversamento della lista ed elaborare direttamente in quale intervallo cade ogni elemento, incrementando il contatore opportuno.

Nella sezione precedente abbiamo preso un indice i e lo abbiamo moltiplicato per AmpiezzaIntervallo per trovare il limite inferiore di un determinato intervallo. Quello che vogliamo fare ora è ricavare direttamente l'indice dell'intervallo cui un valore appartiene.

Questo problema è esattamente l'inverso del precedente: dobbiamo indovinare in quale intervallo cade un valore dividendo quest'ultimo per AmpiezzaIntervallo invece di moltiplicare un indice per AmpiezzaIntervallo.

Dal momento che AmpiezzaIntervallo = 1.0 / NumIntervalli, dividere per AmpiezzaIntervallo è lo stesso di moltiplicare per NumIntervalli. Se moltiplichiamo un numero nella gamma da 0.0 a 1.0 per NumIntervalli otteniamo un numero compreso tra 0.0 e NumIntervalli. Se arrotondiamo questo risultato all'intero inferiore otteniamo proprio quello che stavamo cercando: l'indice dell'intervallo dove cade il valore.

NumIntervalli = 8
Conteggio = [0] * NumIntervalli
for i in Lista:
  Indice = int(i * NumIntervalli)
  Conteggio[Indice] = Conteggio[Indice] + 1

Abbiamo usato la funzione int per convertire un numero in virgola mobile in un intero.

Esercizio: È possibile per questo calcolo produrre un indice che sia fuori dalla gamma di numeri ammessa (negativo o più grande di len(Conteggio)-1)?

Una lista come Conteggi che contiene il numero dei valori per una serie di intervalli è chiamata istogramma.

Esercizio: scrivi una funzione chiamata Istogramma che prende una lista ed il numero di intervalli da considerare e ritorna l'istogramma della distribuzione dei valori per ciascun intervallo.

9.9 Glossario

Tipo immutabile
tipo in cui i singoli elementi non possono essere modificati. L'operazione di assegnazione ad elementi o porzioni produce un errore.
Tipo mutabile
tipo di dati in cui gli elementi possono essere modificati. Liste e dizionari sono mutabili; stringhe e tuple non lo sono.
Tupla
tipo di sequenza simile alla lista con la differenza di essere immutabile. Le tuple possono essere usate dovunque serva un tipo immutabile, per esempio come chiave in un dizionario.
Assegnazione ad una tupla
assegnazione di tutti gli elementi della tupla usando un'unica istruzione di assegnazione.
Programma deterministico
programma che esegue le stesse operazioni ogni volta che è eseguito.
Pseudocasuale
sequenza di numeri che sembra essere casuale ma in realtà è il risultato di un'elaborazione deterministica.
Istogramma
lista di interi in cui ciascun elemento conta il numero di volte in cui una determinata condizione si verifica.
Pattern matching
piano di sviluppo del programma che consiste nell'identificare un tracciato di elaborazione già visto e modificarlo per ottenere la soluzione di un problema simile.


Next Up Previous Hi Index