2: I DIZIONARI

UN NUOVO TIPO DI DATO

La lista di Python è un tipo di dato molto versatile, perchè permette di aggregare facilmente in una sola variabile molti dati anche di tipo diverso tra loro. Essa mostra però i suoi limiti quando si raggiunge una certa complessità (ad esempio quando gli elementi di una lista sono a loro volta delle liste). Immaginiamo, ad esempio, di voler rappresentare con una lista l'alunno di una scuola; probabilmente sarebbe opportuno compilare per prima cosa una tabella per ricordare cosa rappresentano i vari elementi:

Indice Tipo Significato
0 str (stringa) Nome
1 str Cognome
2 int (intero) Anno di corso
3 str Sezione
4 list di int Voti
5 list di int Assenze

e poi potremmo scrivere un'istruzione di assegnazione di questo tipo:


alunno = ["Mario", "Rossi", 2, "B", [7, 7, 6, 5, 6, 8, 9], [3, 1, 2, 0, 1, 1]]

Il limite di un simile approccio sta nel fatto che gli elementi di una lista sono indicizzati per mezzo della loro posizione: se ora volessimo compiere delle operazioni sulla variabile alunno dovremmo ricordarci che, ad esempio, il suo voto in Matematica si trova in alunno[4][3], le assenze in Italiano in alunno[5][0] e così via, ottenendo del codice difficile da scrivere e da leggere senza consultare continuamente la tabella sopra.

Per queste situazioni esiste in Python un tipo di dato più evoluto, il dizionario (dictionary in Inglese, dict in Python): esso è una lista i cui elementi, anzichè attraverso la loro posizione, sono indicizzati attraverso una chiave alfabetica. Ciò significa che, nell'esempio precedente, anzichè scrivere alunno[0] o alunno[1] potremo scrivere alunno["Nome"] o alunno["Cognome"].

Un dizionario è un insieme di coppie chiave-dato: il primo elemento della coppia contiene la chiave (cioè il nome del dato), il secondo il suo valore. La sintassi per dichiarare un dizionario non è proprio semplicissima: vediamone qualche esempio in IDLE:


>>> persona1 = {"Nome":"Luigi", "Cognome":"Bianchi", "Eta":35}
>>> persona2 = {"Nome":"Pietro", "Cognome":"Verdi", "Eta":38}

Per dichiarare una variabile di tipo dizionario bisogna usare le parentesi graffe (tra l'altro piuttosto scomode sulla tastiera italiana, si ottengono premendo insieme <Alt Gr>, <Shift> ed il tasto con le parentesi quadre). All'interno di esse vanno dichiarate, separate da virgole, una serie di coppie chiave:valore, dove chiave è una stringa che sarà il nome del dato, mentre valore è il dato vero e proprio (che può al solito essere di qualsiasi tipo). A questo punto potremo selezionare i vari elementi per mezzo del loro nome:


>>> persona1["Nome"]

'Luigi'

>>> persona2["Eta"]

38

Naturalmente questa sintassi permette sia di leggere che di scrivere i dati. Ad esempio, se oggi è il compleanno di Pietro posso scrivere:


>>> persona2["Eta"] = persona2["Eta"] + 1     # oppure, piu' semplice: persona2["Eta"] += 1
>>> persona2

{'Nome': 'Pietro', 'Cognome': 'Verdi', 'Eta': 39}

Ecco un esempio più complicato: realizziamo ora la memorizzazione del nostro alunno, usando un dizionario che contiene a sua volta due dizionari. Vi ricordo che, come accade anche per le liste, per accedere agli elementi di un dizionario "dentro ad un altro dizionario" si devono usare due volte le parentesi quadre:


>>> voti = {"Italiano":7, "Storia":7, "Inglese":6, "Matematica":5,
	    "Disegno":6, "Ed Fisica":8, "Condotta":9}
>>> assenze= {"Italiano":3, "Storia":1, "Inglese":2, "Matematica":0,
	    "Disegno":1, "Ed Fisica":1}
>>> alunno = {"Nome":"Mario", "Cognome":"Rossi","Classe":3,
	    "Sezione":"B", "Voti":voti, "Assenze":assenze}
>>> alunno["Nome"]

'Mario'

>>> alunno["Voti"]["Matematica"]

5

Notate l'ultima istruzione: la prima parentesi quadra seleziona il dato che ha chiave "Voti" (cioè il dizionario voti) e la seconda seleziona al suo interno il dato che ha chiave "Matematica".

Dato che la dichiarazione di un dizionario può essere molto lunga da leggere, alcuni programmatori, per renderla più chiara, usano andare a capo dopo ogni coppia chiave:valore (dopo la virgola). Ricordate che andando a capo all'interno di una parentesi aperta Python capisce che l'istruzione continua nella riga successiva e non tiene conto dell'indentazione


>>> voti = {
    "Italiano":7,
    "Storia":7,
    "Inglese":6,
    "Matematica":5,
    "Disegno":6,
    "Ed Fisica":8,
    "Condotta":9}

In alternativa possiamo dichiarare un dizionario inizialmente vuoto ed aggiungere via via le coppie chiave:valore semplicemente assegnando loro un valore


>>> persona3 = {}
>>> persona3["Nome"] = "Carlo"
>>> persona3["Cognome"] = "Bruni"
>>> persona3["Eta"] = 28
>>> persona3

{'Nome': 'Carlo', 'Cognome': 'Bruni', 'Eta': 28}
ESERCIZIO 2.1: Aiutate un veterinario a rappresentare mediante un dizionario Python gli animali domestici dei suoi pazienti: dovete inserire tra i dati da ricordare queste chiavi: il nome dell'animale, la specie (cane, gatto, pappagallo ...), la razza (pitbull, cocker, meticcio ...), l'età, il nome del proprietario. Create due variabili diverse animale1 e animale2 che rappresentino due animali.
ESERCIZIO 2.2 Fate lo stesso con due automobili: memorizzate la marca, i modello, la cilindrata e l'anno di immatricolazione.
NOTA: Potete risolvere questi esercizi o dichiarando l'intero dizionario in una sola istruzione, oppure aggiungendo i dati un'istruzione per volta.

FUNZIONI SUI DIZIONARI

Oltre ad assegnare e leggere valori, con i dizionari possiamo fare molte altre cose: Python definisce infatti un buon numero di funzioni che agiscono su di essi. La maggior parte di queste funzioni vanno chiamate usando la sintassi con il punto nome_dizionario.nome_funzione(argomenti) (le funzioni chiamate in questo modo sono dette metodi).

Iniziamo con la fromkeys() che ha questo paradigma:

fromkeys(iterable, value=None) -> dict

La funzione prende come parametro un iterable (lista o tuple) che contiene delle stringhe e restituisce un dizionario che usa come chiavi le stringhe date; i corrispondenti valori sono tutti inizializzati a value, o a None (la keyword di Python per "nessun valore") se omettiamo il parametro opzionale. La chiamata a questa funzione ha però un piccolo problema: dato che essa è un metodo ma non è in genere applicata a nessun dizionario preesistente (proprio perchè serve a crearne uno nuovo), essa va chiamata usando, a sinistra del punto, un dizionario vuoto (come variabile temporanea) oppure la parola dict, così:


>>> nuovo_dict = dict.fromkeys(["a", "b", "c"])     # ok
>>> nuovo_dict

{'a': None, 'b': None, 'c': None}

>>> nuovo_dict = {}.fromkeys(["a", "b", "c"], 0)    # va bene anche questa
>>> nuovo_dict

{'a': 0, 'b': 0, 'c': 0}

>>> nuovo_dict = fromkeys(["a", "b", "c"])          # questa invece no
Traceback (most recent call last):
  File "", line 1, in 
    d1 = fromkeys(k)
NameError: name 'fromkeys' is not defined

Questa funzione è utile in quelle situazioni nelle quali dobbiamo memorizzare un gran numero di dizionari tutti basati sulle stesse chiavi. Ad esempio, riprendiamo il nostro esempio di un alunno di una scuola: una volta deciso quali dati dobbiamo memorizzare dovremo probabilmente introdurre i dati di tutti gli alunni iscritti: ognuno di essi sarà un dizionario basato sulle stesse chiavi. Vediamo un esempio:

NUOVO PROGRAMMA: gestione_scuola.py

Create un nuovo file nell'editor di testo di IDLE e copiate le righe di codice qui sotto:


# definiamo le nostre chiavi
chiavi_alunno = ("Nome", "Cognome", "Anno di corso", "Sezione", "Voti", "Assenze")

def inserisci_alunno():
    # crea un alunno con tutti i valori impostati a None
    alunno = dict.fromkeys(chiavi_alunno)
	# ora la funzione ci chiede di inserire i dati
    print("Inserire i dati dell'alunno")
    for st in chiavi_alunno[:-2]:
        alunno[st] = input(st + ": ")    
    return alunno
	
a = inserisci_alunno()
print(a)

Questo semplice programma chiama la funzione inserisci_alunno() che permette all'utente di inserire i dati anagrafici di un alunno e li restituisce sotto forma di dizionario. Nella riga #6 crea il dizionario alunno con la fromkeys() e nella #9 inizia un ciclo for nel quale il contatore st varia sulle chiavi del dizionario (cioè le stringhe "Nome" "Cognome", ecc.). Notate l'uso della slice notation studiata nel capitolo precedente per escludere le ultime due chiavi (infatti qui vogliamo inserire solo i dati anagrafici, i voti e le assenze presumibilmente saranno inseriti successivamente e quindi rimangono impostati a None).

ESERCIZIO 2.3: Modificate il programma in modo che chiami la funzione all'interno di un ciclo, facendo introdurre 5 alunni, stampando i loro dati e aggiungendoli uno dopo l'altro ad una lista alunni (inizialmente vuota).

Nell'esempio precedente abbiamo usato un ciclo sulle chiavi del dizionario per inserire i dati; casi come questo sono abbastanza frequenti, ed in Python esistono funzioni apposite per facilitare la scrittura dei cicli.

Funzione Significato
keys() Restituisce un oggetto iterabile contenente tutte le chiavi del dizionario, nell'ordine in cui sono state inserite
values() Restituisce un oggetto iterabile contenente tutti i valori del dizionario, nell'ordine in cui sono stati inseriti
items() Restituisce un oggetto iterabile contenente tutte le coppie ("chiave", "valore") del dizionario sotto forma di tuple.

Che cos'è un oggetto iterabile? E' qualcosa di molto simile al range: un oggetto formato da più elementi, che permette di eseguire un ciclo for su di esso ma non di modificare (e nemmeno di selezionare con le parentesi quadre) i singoli elementi. Infatti, come vi ho detto, queste funzioni vengono usate di solito per generare dei cicli; se la variabile d è un dizionario, allora:


for k in d.keys():
    .  .  .    # ciclo sulle chiavi del dizionario
for v in d.values():
    .  .  .    # ciclo sui valori del dizionario
for i in d.items():
    .  .  .    # ciclo sulle coppie chiave-valore

La scelta di non restituire una lista è dovuta essenzialmente a motivazioni di efficienza: come sapete le liste sono i tipi di dato che occupano più memoria, ed in fondo non avrebbe molto senso ottenere da un dizionario la lista delle sue chiavi e poi modificarla (perchè questo non modificherebbe il dizionario). In ogni caso, se proprio ne avessimo bisogno potremmo sempre applicare la funzione list() al risultato della keys() (ma ATTENZIONE! Vi ripeto che, in questo caso, stiamo modificando la lista ottenuta, ma NON il dizionario da cui l'abbiamo presa). Vediamo qualche esempio per chiarirci le idee:

ESERCIZIO 2.4: Se non l'avete già fatto, ridefinite in IDLE il dizionario persona1 come abbiamo fatto sopra:

>>> persona1 = {"Nome":"Luigi", "Cognome":"Bianchi", "Eta":35}
Adesso scrivete le seguenti istruzioni, e notate la risposta di Python quando premete INVIO:

>>> persona1.keys()
>>> persona1.values()
>>> persona1.items()
>>> k = persona1.keys()
>>> k.append("Indirizzo")
>>> k[0]
>>> k = list(k)
>>> k.append("Indirizzo")
>>> k
>>> persona1

Nelle righe #5 e #6 noterete che è impossibile modificare il risultato della keys() o leggere un singolo elemento. Nella #7 convertiamo k in una lista e la modifichiamo, ma, come si vede nella #10, il dizionario persona1 rimane invariato.

ESERCIZIO 2.5: Sempre partendo dal dizionario persona1, scrivete tre piccoli cicli for che stampino, una dopo l'altra, le sue chiavi, i suoi valori e le coppie chiave-valore. ATTENZIONE! Potete fare questo esercizio anche in IDLE (senza bisogno di usare l'editor che vi richiede di salvare il file): scrivete il for e premete <INVIO> (IDLE indenterà automaticamente la riga successiva), poi scrivete il corpo del ciclo (basta una sola riga) e premete due volte <INVIO> per eseguire tutte e due le righe.

Riprendiamo infine il nostro programma gestione_scuola.py e facciamo qualche miglioramento:

ESERCIZIO 2.6: L'istruzione print(a) alla fine del programma stampa la variabile sotto forma di dizionario, con le chiavi e i valori tra parentesi graffe. Se vogliamo stampare solo i valori inseriti possiamo sostituirla con queste righe:

	.   .   .
	
for st in a.values():
    print(st, end=" ")
print()
Ricordate che end è uno dei parametri opzionali della funzione print(), che indica il carattere da aggiungere dopo l'ultimo argomento: se lo omettiamo la funzione stampa un carattere di fine riga (e quindi va a capo ogni volta che viene chiamata, come sappiamo. Scrivendo invece end=" " le facciamo stampare uno spazio, in modo che tutti i valori siano stampati sulla stessa riga (e si va a capo solo alla fine del ciclo, quando si esegue la print() finale).
ESERCIZIO 2.7 Infine creiamo un'altra funzione per inserire i voti e le assenze dell'alunno.
  1. Aggiungete, subito dopo la lista chiavi_alunno, quest'altra:
    
    chiavi_materie = {"Italiano", "Storia", "Inglese", "Matematica", "Disegno", "Ed Fisica", "Condotta"}
        
  2. Definite, dopo la inserisci_alunno(), quest'altra funzione:
    
    def inserisci_voti(alunno):
        alunno["Voti"] = fromkeys(chiavi_materie)
        for k in alunno["Voti"].keys():
            alunno["Voti"][k] = input("Voto in " + k + ": ")
        return alunno
    
  3. Chiamate la funzione dal programma principale, dopo aver inserito i dati anagrafici dell'alunno.
ESERCIZIO 2.8 Modificate la funzione inserisci_voti() in modo che insieme ai voti (nello stesso ciclo) si possano inserire anche le assenze (tenete presente che i due dizionari che rappresentano i voti e le assenze hanno le stesse chiavi).

Infine elenchiamo altre importanti funzioni che ci permettono di manipolare i dizionari:

Funzione Significato
update(lista) Questa funzione accetta come parametro un altro dizionario o una lista di coppie chiave-valore. Per ogni coppia, se la chiave è già presente aggiorna il suo valore, altrimenti la aggiunge al dizionario.
pop("chiave", [default]) -> valore Elimina dal dizionario la coppia con la chiave specificata, restituendone il valore del dato. Se la chiave non è presente restituisce il parametro opzionale default, oppure provoca un errore se esso non è presente.
popitem() -> ("chiave", valore) Elimina dal dizionario l'ultima coppia aggiunta, restituendone la chiave ed il valore sotto forma di tuple. Se il dizionario è vuoto provoca un errore.
clear() Cancella tutte le coppie dal dizionario, che diventa così un dizionario vuoto

Vediamo un esempio dell'uso di queste funzioni:


>>> persona1 = {"Nome":"Luigi", "Cognome":"Bianchi", "Eta":35}
>>> persona1.update({"Eta":36, "Stato":"Celibe"})
>>> persona1

{'Nome':'Luigi', 'Cognome':'Bianchi', 'Eta':36, 'Stato':'Celibe'}

>>> persona1.pop("Eta")

36

>>> persona1

{'Nome':'Luigi', 'Cognome':'Bianchi', 'Stato':'Celibe'}

>>> persona1.pop("Eta")

36

>>> persona1

{'Nome':'Luigi', 'Cognome':'Bianchi', 'Stato':'Celibe'}

>>> persona1.pop("Eta")

Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    persona1.pop("Eta")
KeyError: 'Eta'

>>> persona1.pop("Eta", 0)

0

Fine della lezione