4.5. Strutture di controllo#

Python gestisce il flusso di esecuzione tramite le tipiche strutture di controllo di sequenza, selezione e iterazione. La sequenza viene implementata semplicemente indicando le istruzioni, una per riga, in ordine di esecuzione: per esempio la cella seguente crea due liste, una con nomi di supereroi e un'altra con i corrispondenti anni di prima apparizione, e le memorizza nelle variabili names e years.

names = ['Aquaman', 'Ant-Man', 'Batman', 'Black Widow',
         'Captain America', 'Daredavil', 'Elektra', 'Flash',
         'Green Arrow', 'Human Torch', 'Hancock', 'Iron Man',
         'Mystique', 'Professor X', 'Rogue', 'Superman',
         'Spider-Man', 'Thor', 'Northstar']

years = [1941, 1962, None, None, 1941,
         1964, None, 1940, 1941, 1961,
         None, 1963, None, 1963, 1981,
         None, None, 1962, 1979]

Il valore speciale None è stato utilizzato nei casi in cui non risulta disponibile l'anno di prima apparizione di un supereroe. In queste situazioni si parla di valori mancanti (o si utilizza l'equivalente termine inglese missing values) che di norma vengono indicati con la sigla NA (dall'inglese "not available"). La scelta di None come valore per codificare gli elementi mancanti è puramente arbitraria: l'eterogeneità delle liste ci avrebbe permesso di utilizzare per esempio la stringa 'NA' o altri valori (anche espressioni numeriche che non indicano un anno). In realtà vedremo più avanti che esistono altre modalità che permettono di memorizzare ed elaborare i dati in modo più agevole.

Immaginiamo di voler conteggiare, anno per anno, il numero totale di apparizioni, calcolando quelle che in statistica vengono chiamate le frequenze assolute del numero di apparizioni. Un approccio classico è quello di utilizzare un contatore per ogni anno, scandire la lista delle prime apparizioni e incrementare di volta in volta il contatore corrispondente all'anno trovato. Una struttura dati particolarmente adeguata per aggregare i contatori è un dizionario, in cui le chiavi corrispondono agli anni. La scansione di una lista viene effettuata in python da una delle due strutture iterative, il ciclo for. A differenza di quanto succede di norma, non si tratta di un ciclo numerato bensì di un ciclo che esegue il suo corpo in corrispondenza di ogni elemento di un oggetto iterabile. Liste e tuple sono appunto gli esempi più semplici di oggetti iterabili: immaginando che lista sia una lista da scandire (ma andrebbe bene anche una tupla) e che esista una funzione elabora che accetta un argomento, la sintassi

for elemento in lista:
    elabora(elemento)

implementa appunto un ciclo for, in cui elemento rappresenta una variabile che conterrà a ogni iterazione uno degli elementi. In particolare, liste e tuple vengono scandite in ordine di posizione, quindi elemento conterrà il primo elemento di lista durante la prima iterazione, il suo secondo elemento durante la seconda iterazione e così via. Notate anche che le funzioni si invocano usando la sintassi tipica basata sull'uso di parentesi tonde. Infine, va sottolineato che la seconda riga inizia più a destra della prima: in molti linguaggi questa tecnica (detta di indentazione) ha lo scopo puramente visuale di mettere in evidenza l'istruzione o le istruzioni che vengono ripetute nel ciclo (il corpo del ciclo). In python l'indentazione è invece obbligatoria per indicare quali sono le istruzioni che compongono il corpo del ciclo (cosa che negli altri linguaggi viene fatta utilizzando per esempio le parentesi graffe). Non esiste una regola prefissata che indichi come effettuare l'indentazione: si può usare un carattere di tabulazione, oppure alcuni caratteri di spazio. l'unica limitazione è quella di mantenere la stessa scelta una volta che questa è stata fatta: se si decide di indentare il corpo di un ciclo usando, per esempio, tre spazi, tutte le istruzioni del corpo dovranno essere indentate di tre spazi.

Tornando al problema di effettuare il conteggio del numero di apparizioni al variare degli anni, un primo tentativo che utilizza un dizionario per memorizzare i relativi contatori potrebbe essere il seguente:

# non funziona!
counts = {}
for y in years:
    counts[y] += 1

In realtà tale codice non funzionerebbe, perché la prima istruzione crea un dizionario counts vuoto, e quindi il primo accesso che verrebbe fatto utilizzerebbe una chiave che non esiste, causando il lancio di un'eccezione. È pertanto necessario verificare di volta in volta che l'anno considerato sia una chiave esistente (il che significa che l'anno considerato è già stato trovato precedentemente, e quindi il corrispondente contatore esiste già e va solamente incrementato) oppure no (e dunque il contatore va inizializzato). È quindi necessario utilizzare l'operatore in unitamente a una struttura di selezione, e precisamente una if-else. La sintassi di questa struttura è la seguente:

if <condizione>:
    <istruzione_se_condizione_vera>
else:
    <istruzione_se_condizione_falsa>

e la sua semantica è quella che ci si aspetta: la condizione tra la parola chiave if e il carattere di due punti viene valutata: se risulta vera viene eseguita l'istruzione alla linea seguente, altrimenti viene eseguita l'istruzione dopo la parola chiave else. Anche in questo caso l'indentazione permette di identificare quali istruzioni debbano essere eseguite nei due rami della selezione. La cella seguente contiene un'implementazione (stavolta funzionante) del codice che conteggia le apparizioni per anno.

counts = {}
for y in years:
    if y in counts:
        counts[y] += 1
    else:
        counts[y] = 1

Il risultato è il seguente:

counts
{1941: 3,
 1962: 2,
 None: 7,
 1964: 1,
 1940: 1,
 1961: 1,
 1963: 2,
 1981: 1,
 1979: 1}

Notate che una coppia fa riferimento alla chiave None, che sarà relativa al numero di casi mancanti. Supponiamo di voler visualizzare i conteggi visualizzando prima l'anno con il maggior numero di apparizioni, per poi procedere in ordine decrescente. Un possibile modo di procedere è quello di "convertire" counts nella corrispondente tupla di coppie e poi ordinare quest'ultima. La prima operazione si effettua facilmente invocando sul dizionario il metodo items. La seconda operazione è più complessa, perché è necessario basare l'ordinamento sul secondo elemento di ogni coppia. Nella cella seguente si utilizza l'argomento opzionale key della funzione sorted e una funzione anonima per specificare il criterio su cui basare l'ordinamento. L'esempio utilizza anche l'argomento opzionale reverse per ottenere gli anni ordinati a partire da quello con il maggior numero di apparizioni.

pairs = list(counts.items())
sorted(pairs, key=lambda p:p[1], reverse=True)
[(None, 7),
 (1941, 3),
 (1962, 2),
 (1963, 2),
 (1964, 1),
 (1940, 1),
 (1961, 1),
 (1981, 1),
 (1979, 1)]