Liste innestate in Python

Le liste innestate, comunemente chiamate "liste di liste", rappresentano un concetto fondamentale nella programmazione Python. Esse permettono di creare strutture dati multidimensionali, consentendo agli sviluppatori di gestire in modo efficiente dati complessi e organizzati.

In questa lezione vedremo come creare e manipolare liste innestate in Python, e come utilizzarle per risolvere problemi complessi. Vedremo anche qualche esempio pratico di applicazione delle liste innestate, come la rappresentazione di matrici e tabelle.

Cos'è una Lista Innestata?

Una lista innestata è una lista che contiene altre liste come elementi. In Python, le liste possono contenere elementi di qualsiasi tipo, incluso un'altra lista. La possibilità di creare una struttura gerarchica di liste è particolarmente utile quando si tratta di dati strutturati, come matrici, tabelle o elenchi di elementi correlati. La sintassi per creare una lista innestata è molto semplice:

lista_innestata = [[elemento1, elemento2], [elemento3, elemento4], [elemento5, elemento6]]

Ogni lista interna può contenere un numero diverso di elementi, e queste liste possono essere a loro volta innestate all'interno di altre liste, creando così strutture dati complesse a più livelli.

In Python è possibile creare liste di liste anche assegnando una lista ad un elemento di un'altra lista. Ad esempio, la seguente istruzione crea una lista innestata:

lista_interna = [elemento1, elemento2]
lista_esterna = [lista_interna, elemento3]

La lista di sopra lista_esterna contiene due elementi: la lista lista_interna e l'elemento elemento3. La lista lista_interna a sua volta contiene due elementi: elemento1 e elemento2.

Ricapitolando:

Definizione

Lista Innestata o Lista di Liste

Una lista innestata è una lista che contiene altre liste come elementi al proprio interno.

Si può definire una lista innestata o lista di liste in fase di creazione della lista dichiarando le liste interne tra parentesi quadre e separate da virgole:

lista_innestata = [[elemento1, elemento2], [elemento3, elemento4], [elemento5, elemento6]]

oppure assegnando una lista ad un elemento di un'altra lista:

lista_interna = [elemento1, elemento2]
lista_esterna = [lista_interna, elemento3]

Accesso agli elementi di una Lista Innestata

Per accedere agli elementi di una lista innestata bisogna tener presente che ogni elemento della lista esterna è a sua volta una lista. Per accedere agli elementi di una lista innestata bisogna quindi utilizzare due indici: il primo per accedere all'elemento della lista esterna, il secondo per accedere all'elemento della lista interna.

Chiariamo con un esempio. Consideriamo la seguente lista innestata:

lista_innestata = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Supponiamo di voler accedere all'elemento 5 della lista innestata. Per farlo, dobbiamo utilizzare due indici: il primo per accedere alla seconda lista, il secondo per accedere all'elemento 5:

print(lista_innestata[1][1])  # Output: 5

In questo esempio, lista_innestata[1] restituisce la seconda lista [4, 5, 6], mentre lista_innestata[1][1] restituisce l'elemento 5 della seconda lista.

In sostanza l'indicizzazione in questo caso è di tipo gerarchico, e si può rappresentare come una struttura ad albero:

lista_innestata
|
├─[0]-------┬-[0]------1
|           ├-[1]------2
|           └-[2]------3
|
├─[1]-------┬-[0]------4
|           ├-[1]------5
|           └-[2]------6
|
└─[2]-------┬-[0]------7
            ├-[1]------8
            └-[2]------9

Il discorso può essere esteso anche al caso in cui i livelli di innestamento siano più di due. Consideriamo la seguente lista innestata:

lista_innestata = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9, [10, 11, 12]]
]

Se rappresentiamo la lista innestata come un albero, otteniamo la seguente struttura:

lista_innestata
|
├─[0]-------┬-[0]------1
|           ├-[1]------2
|           └-[2]------3
|
├─[1]-------┬-[0]------4
|           ├─[1]------5
|           └─[2]------6
|
└─[2]-------┬-[0]------7
            ├─[1]------8
            ├─[2]------9
            └─[3]------┬-[0]------10
                       ├─[1]------11
                       └─[2]------12

Adesso, volendo accedere all'elemento 11, dobbiamo utilizzare tre indici:

print(lista_innestata[2][3][1])  # Output: 11

Ricapitolando:

Definizione

Indicizzazione Gerarchica

Per accedere agli elementi di una lista innestata bisogna utilizzare un numero di indici pari al numero di livelli di innestamento.

La sintassi per accedere agli elementi di una lista innestata è la seguente:

lista_innestata[indice1][indice2]...[indiceN]

Ogni indice è relativo ad un livello di innestamento.

Modifica di una Lista Innestata

Per modificare un elemento di una lista innestata bisogna utilizzare la sintassi per l'indicizzazione gerarchica:

lista_innestata[indice1][indice2]...[indiceN] = nuovo_valore

Consideriamo l'esempio precedente:

lista_innestata = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Supponiamo di voler modificare l'elemento 5 con il valore 10. Per farlo, dobbiamo utilizzare due indici: il primo per accedere alla seconda lista, il secondo per accedere all'elemento 5:

lista_innestata[1][1] = 10
print(lista_innestata)  # Output: [[1, 2, 3], [4, 10, 6], [7, 8, 9]]

Eliminazione di elementi da una Lista Innestata

Anche l'eliminazione di un elemento da una lista innestata richiede l'utilizzo della sintassi per l'indicizzazione gerarchica:

del lista_innestata[indice1][indice2]...[indiceN]

Consideriamo l'esempio precedente:

lista_innestata = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Supponiamo di voler eliminare l'elemento 5. Per farlo, dobbiamo utilizzare due indici: il primo per accedere alla seconda lista, il secondo per accedere all'elemento 5:

del lista_innestata[1][1]
print(lista_innestata)  # Output: [[1, 2, 3], [4, 6], [7, 8, 9]]

Aggiunta di elementi ad una Lista Innestata

Come nei casi precedenti, anche l'aggiunta di un elemento ad una lista innestata richiede l'utilizzo della sintassi per l'indicizzazione gerarchica:

lista_innestata[indice1][indice2]...[indiceN].append(nuovo_elemento)

Consideriamo l'esempio precedente:

lista_innestata = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Supponiamo di voler aggiungere l'elemento 10 alla terza lista. Per farlo, dobbiamo utilizzare due indici: il primo per accedere alla terza lista, il secondo per aggiungere l'elemento 10:

lista_innestata[2].append(10)
print(lista_innestata)  # Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

A questo punto la terza lista interna conterrà quattro elementi anziché tre.

Applicazione Pratica: Matrici

Le matrici sono una struttura dati bidimensionale composta da righe e colonne, spesso utilizzate per rappresentare dati matematici. Le liste innestate sono un meccanismo naturale per la rappresentazione delle matrici in Python.

Consideriamo la seguente matrice:

\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix}

La matrice può essere rappresentata in Python come una lista innestata:

matrice = [
    [1, 2, 3],      # Riga 0
    [4, 5, 6],      # Riga 1
    [7, 8, 9]       # Riga 2
]

Ogni lista interna rappresenta una riga della matrice, e gli elementi all'interno di ciascuna lista rappresentano i valori delle colonne corrispondenti.

Accedere agli elementi di una matrice è molto semplice: basta utilizzare due indici, uno per la riga e uno per la colonna, ricordando, però, che gli indici partono da zero. Per cui se vogliamo accedere all'elemento a_{ij}, dobbiamo utilizzare gli indici i-1 e j-1.

Ad esempio, volendo accedere all'elemento a_{23}, dobbiamo utilizzare gli indici 1 e 2:

a23 = matrice[1][2]
print(a23)  # Output: 6

Esempio: Somma di due Matrici

Supponiamo di voler sommare due matrici A e B:

A = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \qquad B = \begin{bmatrix} 9 & 8 & 7 \\ 6 & 5 & 4 \\ 3 & 2 & 1 \end{bmatrix}

Utilizzando liste innestate, possiamo facilmente implementare questa operazione. Per prima cosa, dobbiamo creare due liste innestate, una per la matrice A e una per la matrice B:

A = [
    [1, 2, 3],      # Riga 0
    [4, 5, 6],      # Riga 1
    [7, 8, 9]       # Riga 2
]

B = [
    [9, 8, 7],      # Riga 0
    [6, 5, 4],      # Riga 1
    [3, 2, 1]       # Riga 2
]

Successivamente possiamo definire una funzione che somma due matrici:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def somma_matrici(A, B, n, m):
    # Creazione della matrice risultato
    C = [[0 for j in range(m)] for i in range(n)]

    # Somma delle matrici
    for i in range(n):
        for j in range(m):
            C[i][j] = A[i][j] + B[i][j]

    # Restituzione della matrice risultato
    return C

La funzione somma_matrici prende in input due matrici A e B, il numero di righe n e il numero di colonne m e restituisce la matrice risultato C.

Per prima cosa, alla riga 3, viene creata la matrice risultato C utilizzando una combinazione tra la sintassi per la creazione di liste innestate e la sintassi per le Comprensioni di Lista.

Successivamente, viene effettuata la somma delle matrici A e B utilizzando un doppio ciclo for nelle righe 6-8. Infine, viene restituita la matrice risultato C.

Per testare la funzione somma_matrici, possiamo utilizzare il seguente codice:

C = somma_matrici(A, B, 3, 3)
print(C)  # Output: [[10, 10, 10], [10, 10, 10], [10, 10, 10]]

Il problema di questo approccio è che la funzione somma_matrici prende in ingresso le dimensioni delle matrici A e B. Possiamo semplificare l'utilizzo della funzione somma_matrici utilizzando la funzione len per calcolare le dimensioni delle matrici A e B:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def somma_matrici_versione_2(A, B):
    # Calcolo delle dimensioni delle matrici
    n = len(A)
    m = len(A[0])

    # Creazione della matrice risultato
    C = [[0 for j in range(m)] for i in range(n)]

    # Somma delle matrici
    for i in range(n):
        for j in range(m):
            C[i][j] = A[i][j] + B[i][j]

    # Restituzione della matrice risultato
    return C

La differenza, rispetto alla prima versione della funzione, è che la funzione somma_matrici_versione_2 non prende in ingresso le dimensioni delle matrici A e B, ma le calcola utilizzando la funzione len nelle righe 2-3. In particolare, per calcolare il numero di colonne della matrice A, viene utilizzata la funzione len sulla prima riga della matrice A.

Questo funziona fintanto che tutte le righe della matrice A hanno la stessa lunghezza. Se, invece, le righe della matrice A hanno lunghezze diverse, la funzione somma_matrici_versione_2 non funziona correttamente.

Adesso possiamo utilizzare la funzione somma_matrici_versione_2 in questo modo:

C = somma_matrici_versione_2(A, B)
print(C)  # Output: [[10, 10, 10], [10, 10, 10], [10, 10, 10]]

Esempio: Prodotto di una Matrice per uno Scalare

Supponiamo di voler moltiplicare una matrice A per uno scalare k:

A = \begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{bmatrix} \qquad k = 2

Possiamo implementare questa operazione utilizzando una funzione:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def prodotto_matrice_scalare(A, k):
    # Calcolo delle dimensioni della matrice
    n = len(A)
    m = len(A[0])

    # Creazione della matrice risultato
    B = [[0 for j in range(m)] for i in range(n)]

    # Prodotto della matrice per lo scalare
    for i in range(n):
        for j in range(m):
            B[i][j] = A[i][j] * k

    # Restituzione della matrice risultato
    return B

La funzione prodotto_matrice_scalare prende in input una matrice A e uno scalare k e restituisce la matrice risultato B.

Adesso possiamo utilizzare la funzione prodotto_matrice_scalare in questo modo:

B = prodotto_matrice_scalare(A, 2)
print(B)  # Output: [[2, 4, 6], [8, 10, 12], [14, 16, 18]]

Applicazione Pratica: Tabelle

Le liste innestate trovano la loro naturale applicazione anche nella rappresentazione di tabelle di dati.

Supponiamo, ad esempio, di voler memorizzare in una tabella i dati relativi alle principali città italiane. Vogliamo memorizzare in una lista i seguenti dati:

  • Nome della città
  • Regione
  • Numero di abitanti
  • Superficie in km^2

Possiamo rappresentare la tabella utilizzando una lista innestata:

citta = [
    ["Milano", "Lombardia", 1352000, 181.76],
    ["Roma", "Lazio", 2873000, 1285.31],
    ["Napoli", "Campania", 967000, 119.00],
    ["Torino", "Piemonte", 886837, 130.17],
    ["Palermo", "Sicilia", 673735, 158.90],
    ["Genova", "Liguria", 583601, 243.60],
    ["Bologna", "Emilia-Romagna", 388367, 140.70],
    ["Firenze", "Toscana", 382808, 102.41],
    ["Bari", "Puglia", 325052, 116.00],
    ["Catania", "Sicilia", 311584, 180.90]
]

Per accedere ai dati relativi alla città di Milano, possiamo utilizzare la seguente sintassi:

milano = citta[0]
print(milano)  # Output: ['Milano', 'Lombardia', 1352000, 181.76]

Supponiamo di voler ricavare la densità di popolazione di ciascuna città. Possiamo utilizzare la seguente funzione:

1
2
3
4
5
6
def densita_di_popolazione(citta):
    # Calcolo della densità di popolazione
    densita = citta[2] / citta[3]

    # Restituzione della densità di popolazione
    return densita

Adesso possiamo utilizzare la funzione densita_di_popolazione in questo modo:

for c in citta:
    print(f'{c[0]:20} {densita_di_popolazione(c)}')

Il codice precedente stampa la densità di popolazione di ciascuna città:

Milano               7431.986531986532
Roma                 2234.422657952069
Napoli               8134.453781512605
Torino               6814.563106796117
Palermo              4240.911949685534
Genova               2396.296296296296
Bologna              2759.014209591474
Firenze              3739.100049390681
Bari                 2801.448275862069
Catania              1720.4339963833635

In Sintesi

Le liste innestate in Python sono uno strumento potente per la gestione delle matrici e altre strutture dati bidimensionali. La loro capacità di organizzare dati in modo gerarchico e la facilità con cui consentono di accedere agli elementi li rendono essenziali per affrontare problemi complessi che coinvolgono dati tabellari, immagini e algoritmi matematici.

In questa lezione abbiamo visto:

  • Come creare una lista innestata
  • Come accedere agli elementi di una lista innestata
  • Come modificare gli elementi di una lista innestata
  • Come creare una matrice utilizzando una lista innestata
  • Come creare tabelle di dati utilizzando liste innestate