Introduzione alle Comprensioni di Lista in Python

In questa lezione introdurremo un costrutto molto importante nel linguaggio Python: le Comprensioni di Lista. Esse prendono spunto dalla notazione matematica per la definizione di un insieme e sono un modo molto potente per creare liste in Python.

Ci concentreremo sulle basi della sintassi e sulle loro applicazioni alle liste. Vedremo come costruire liste complesse a partire da liste semplici e come applicare filtri.

Comprensioni di Lista

Le Comprensioni di Lista, dall'inglese List Comprehensions, sono un costrutto del linguaggio Python molto potente. Esse sono un concetto molto più generale di quanto vedremo in questa lezione e sono applicabili ad altri tipi di dato. Per tal motivo, in questa lezione ci concentreremo sulle basi e solo sulla loro applicazione alle liste. Nelle prossime lezioni, invece, studieremo il quadro generale.

A dispetto del nome alquanto esoterico, le Comprensioni di Lista traggono la loro origine dal formalismo matematico della teoria degli insiemi. Per questo motivo ci conviene partire un attimo da come, in matematica, rappresentiamo un insieme.

Generalmente, esistono due modi di rappresentare un insieme:

  • Rappresentazione per enumerazione, chiamata anche Rappresentazione estensiva. In questa rappresentazione, l'insieme viene definito elencando tutti i suoi elementi. Ad esempio, l'insieme A dei numeri naturali inferiori a 10 può essere rappresentato come:

    A = \{0, 1, 2, 3, 4, 5, 6, 7, 8, 9\}

    In questo caso abbiamo elencato i singoli elementi, uno ad uno. Tale rappresentazione, tuttavia, diventa problematica quando la cardinalità dell'insieme, ossia il numero di elementi dell'insieme, cresce. Per tal motivo si utilizza molto più frequentemente la seconda rappresentazione.

  • Rappresentazione per caratteristica, chiamata anche Rappresentazione intensiva. In questa rappresentazione, l'insieme viene definito specificando una proprietà che i suoi elementi devono possedere. Ritornando all'esempio di prima, possiamo rappresentare l'insieme A dei numeri naturali inferiori a 10 come:

    A = \{x \in \mathbb{N} \mid x < 10\}

    In questo caso abbiamo specificato che un elemento x appartiene all'insieme A se x è un numero naturale e x è minore di 10. Questa rappresentazione è molto più compatta e, soprattutto, molto più flessibile. Infatti, se volessimo rappresentare l'insieme B dei numeri naturali inferiori a 100, potremmo semplicemente scrivere:

    B = \{x \in \mathbb{N} \mid x < 100\}

    In questo modo, possiamo rappresentare un insieme di cardinalità arbitrariamente grande, semplicemente cambiando il valore di x.

In Python, le Comprensioni di Lista sono un modo per rappresentare una lista attraverso una rappresentazione intensiva. In altre parole, possiamo utilizzare le Comprensioni di Lista per creare una lista specificando una proprietà che i suoi elementi devono possedere.

Ma andiamo per ordine. Per prima cosa bisogna notare, prima che qualche matematico storca il naso, che una lista in Python non è esattamente la stessa cosa di un insieme. Si tratta sicuramente di due concetti molto simili, ma non identici. In particolare, una lista in Python può contenere elementi duplicati, mentre un insieme non può. Inoltre, un insieme non può contenere duplicati, mentre una lista in Python lo può fare.

Detto questo, finora abbiamo sempre creato liste in Python usando ne più ne meno la rappresentazione estensiva, ossia enumerando i singoli elementi. Ad esempio, volendo creare una lista con i primi 10 numeri naturali, abbiamo sempre scritto:

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

Ma se la cardinalità cresce, questo modo di creare una lista diventa problematico. Ecco che entrano in gioco le Comprensioni di Lista.

La sintassi di base di una Comprensione di Lista è la seguente:

[<espressione> for <elemento> in <iterabile>]

Qui abbiamo usato il concetto di iterabile, un concetto avanzato che studieremo nel dettaglio più avanti. Per ora ci basta sapere che un iterabile è un'entità che può essere iterata, ossia i suoi elementi possono essere scorsi uno ad uno. Ad esempio, una lista è un iterabile così come un range.

Ora, se vogliamo creare una lista con i primi 10 numeri naturali, possiamo scrivere:

numeri = [x for x in range(10)]

Prima di esaminare nel dettaglio la sintassi, notiamo che la Comprensione di Lista assomiglia molto alla notazione matematica. Infatti, rassomiglia molto a:

\{x \mid x \in {\rm range}(10)\}

In linguaggio naturale: "insieme di tutti gli elementi x tali che x appartiene all'intervallo di numeri naturali da 0 a 9".

Infatti, se andiamo a stampare il risultato otteniamo:

>>> print(numeri)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Esaminiamo, ora, la sintassi. La prima cosa da notare è che la Comprensione di Lista è racchiusa tra parentesi quadre. Questo indica all'interprete che stiamo andando a creare una lista.

La seconda cosa da notare è che la Comprensione di Lista è composta da tre parti. Esaminiamole in ordine inverso che aiuta meglio la comprensione:

  • Iterabile: è l'ente di cui andiamo a scorrere gli elementi. In questo caso, è un range per cui stiamo scorrendo tutti i numeri naturali da 0 a 9.
  • Elemento: è la variabile che assumerà progressivamente i valori degli elementi dell'iterabile. In questo caso gli abbiamo dato come nome x ma avremmo potuto usare un qualsiasi altro nome.
  • Espressione: ogniqualvolta l'elemento assume un valore preso dall'iterabile, l'espressione viene valutata e il risultato viene aggiunto alla lista. In questo caso, l'espressione è semplicemente x, ossia il valore stesso dell'elemento.

In base a queste informazioni, possiamo scrivere la Comprensione di Lista in modo più esplicito:

lista_finale = []
for elemento in iterabile:
    lista_finale.append(espressione)

In altre parole, la Comprensione di Lista può essere vista come un modo per scrivere un ciclo for di inizializzazione più compatto. Applicato al nostro esempio diventa:

numeri = []
for x in range(10):
    numeri.append(x)

Grazie alle Comprensioni di Lista possiamo anche creare liste con una cardinalità arbitrariamente grande. Ad esempio, se volessimo creare una lista con i primi 100 numeri naturali, potremmo scrivere:

numeri = [x for x in range(100)]

Questa è, in sostanza, la struttura di base di una Comprensione di Lista.

Definizione

Sintassi base di una Comprensione di Lista

La sintassi di base di una Comprensione di Lista è la seguente:

[<espressione> for <elemento> in <iterabile>]

Dove:

  • <iterabile> è l'iterabile di cui andiamo a scorrere gli elementi.
  • <elemento> è la variabile che assumerà progressivamente i valori degli elementi dell'iterabile.
  • <espressione> è l'espressione che viene valutata ogniqualvolta l'elemento assume un valore preso dall'iterabile.

Espressione di una Comprensione di Lista

Nell'introduzione di sopra abbiamo detto che il primo elemento di una Comprensione di Lista è un'espressione. Tuttavia nel nostro esempio abbiamo usato semplicemente il nome dell'elemento. In realtà, possiamo usare qualsiasi espressione complessa a piacimento.

Vediamo un esempio. Partiamo dal creare una lista con i primi 10 numeri naturali:

numeri = [x for x in range(10)]

Ora, se volessimo creare una lista con i primi 10 numeri naturali elevati al quadrato, potremmo scrivere:

numeri_al_quadrato = [x ** 2 for x in numeri]
print(numeri_al_quadrato)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

In questo caso abbiamo creato un'altra lista a partire dalla prima usando:

  • La lista originaria come iterabile. Del resto una lista è una sequenza ordinata di elementi e in quanto tale è un iterabile;
  • Come elemento, abbiamo usato x;
  • Come espressione, abbiamo usato x ** 2, ossia x elevato al quadrato.

Dal punto di vista matematico è come se avessimo scritto:

\{x^2 \mid x \in {\rm range}(10) \}

Proviamo con un altro esempio. Supponiamo di avere una lista di stringhe con i nomi dei giorni della settimana:

giorni = ["lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato", "domenica"]

Possiamo creare una lista con i nomi dei giorni della settimana in maiuscolo scrivendo:

giorni_maiuscolo = [giorno.upper() for giorno in giorni]
print(giorni_maiuscolo)
['LUNEDÌ', 'MARTEDÌ', 'MERCOLEDÌ', 'GIOVEDÌ', 'VENERDÌ', 'SABATO', 'DOMENICA']

Oppure possiamo creare una lista con il numero di lettere che compongono il nome dei giorni della settimana:

numero_lettere = [len(giorno) for giorno in giorni]
print(numero_lettere)
[7, 8, 10, 8, 8, 6, 8]

Comprensioni di Lista e Filtri

Le Comprensioni di Lista non si limitano soltanto a quanto visto sopra. Possiamo anche aggiungere dei filtri. Un filtro è un'espressione booleana che viene valutata ogniqualvolta l'elemento assume un valore preso dall'iterabile. Se l'espressione è vera, allora l'elemento viene passato all'espressione. L'espressione viene valutata e il risultato viene aggiunto alla lista. Se l'espressione è falsa, allora l'elemento viene scartato.

La sintassi per applicare un filtro è molto semplice:

[<espressione> for <elemento> in <iterabile> if <filtro>]

In questo caso abbiamo aggiunto un if dopo la parte di inizializzazione della Comprensione di Lista e un'espressione booleana <filtro>.

Vediamo qualche esempio. Supponiamo di volere creare una lista dei quadrati dei primi 20 numeri interi, ma solo se tali numeri interi sono pari. Possiamo scrivere una Comprensione di Lista con un filtro come segue:

numeri_pari = [x ** 2 for x in range(20) if x % 2 == 0]
print(numeri_pari)

La sintassi è molto semplice e può quasi essere letta come una frase in linguaggio naturale: "Crea una lista composta da x^2, dove x è un numero intero compreso tra 0 e 19, e x è pari".

[0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

Vediamo un altro esempio. Supponiamo di voler creare una lista con i nomi dei giorni della settimana che iniziano con la lettera "m". Possiamo scrivere una Comprensione di Lista con un filtro come segue:

giorni = ["lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato", "domenica"]
giorni_con_m = [giorno for giorno in giorni if giorno.startswith("m")]
print(giorni_con_m)
['martedì', 'mercoledì']

Volendo possiamo anche usare più filtri. Ad esempio, possiamo creare una lista con i nomi dei giorni della settimana che iniziano con la lettera "m" e che hanno una lunghezza maggiore di 7 caratteri:

giorni_con_m_e_lunghezza_maggiore_di_7 = \
    [giorno for giorno in giorni \
        if giorno.startswith("m") and len(giorno) > 7]
print(giorni_con_m_e_lunghezza_maggiore_di_7)
['mercoledì']

Alla fine, anche in caso di Comprensioni di Lista con filtro, il tutto può essere ricondotto ad un ciclo for:

nuova_lista = []
for elemento in iterabile:
    if filtro:
        nuova_lista.append(espressione)

Per cui, la Comprensione di lista:

numeri_pari = [x ** 2 for x in range(20) if x % 2 == 0]

è equivalente a:

numeri_pari = []
for x in range(20):
    if x % 2 == 0:
        numeri_pari.append(x ** 2)
Definizione

Comprensione di Lista e Filtri

Una Comprensione di Lista può contenere un filtro, ossia un'espressione booleana che viene calcolata ogniqualvolta l'elemento assume un valore preso dall'iterabile.

Nel caso in cui l'espressione risulta vera, allora l'elemento viene passato all'espressione. L'espressione viene valutata e il risultato viene aggiunto alla lista. Se l'espressione è falsa, allora l'elemento viene scartato.

La sintassi per applicare un filtro ad una Comprensione di Lista è la seguente:

[<espressione> for <elemento> in <iterabile> if <filtro>]

In Sintesi

Le Comprensioni di Lista sono una delle caratteristiche più interessanti di Python. Sono molto potenti e possono essere usate per creare liste in modo molto semplice e veloce. Inoltre, sono molto leggibili e possono essere usate per scrivere codice molto chiaro e comprensibile.

In questa lezione abbiamo visto i fondamenti delle Comprensioni di Lista. In particolare, abbiamo visto come creare una lista a partire da un iterabile, come calcolare un'espressione per ogni elemento dell'iterabile e come aggiungere un filtro.