{ } Programmazione

Strutture dati in Python: liste, dizionari, insiemi e tuple

Come scegliere la struttura dati giusta in Python: liste, tuple, dizionari, insiemi, deque e Counter a confronto, con costi delle operazioni ed esempi.

Molti problemi di prestazioni e di leggibilità nel codice non dipendono dall'algoritmo, ma da una scelta più semplice: la struttura dati sbagliata. Una lista usata dove servirebbe un insieme può rendere lentissimo un programma con dati reali; un dizionario usato al posto di una classe può rendere il codice difficile da capire. Conoscere bene le strutture di base di Python, e il costo delle operazioni su ciascuna, è uno degli investimenti più redditizi per chi programma.

Liste: sequenze ordinate e modificabili

La lista è la struttura più usata: una sequenza ordinata di elementi, a cui si accede per posizione.

attivita = ["leggere email", "riunione", "revisione codice"]
attivita.append("aggiornare documentazione")
prima = attivita[0]

Le operazioni hanno costi molto diversi:

  • accesso per indice e aggiunta in fondo: veloci, tempo costante (in media per append);
  • inserimento o rimozione all'inizio: lenti, O(n), perché tutti gli elementi vanno spostati;
  • verifica di appartenenza con in: O(n), perché la lista va scorsa.

L'ultimo punto è la fonte di molti rallentamenti. Controllare se un elemento è in una lista di centomila voci, all'interno di un ciclo, produce milioni di confronti.

Tuple: sequenze immutabili

Una tupla è simile a una lista, ma non può essere modificata dopo la creazione. Si usa per gruppi di valori che vanno insieme e non devono cambiare: coordinate, una data scomposta, la riga di un risultato.

punto = (12.5, 41.9)
latitudine, longitudine = punto   # spacchettamento

L'immutabilità ha un vantaggio pratico: le tuple composte da valori immutabili possono essere usate come chiavi di un dizionario o elementi di un insieme, le liste no. Per tuple con molti campi, namedtuple o le dataclass rendono il codice più leggibile, perché permettono di scrivere punto.lat invece di punto[0].

Dizionari: accesso per chiave

Il dizionario associa chiavi a valori, ed è probabilmente la struttura più potente di Python.

prezzi = {"caffè": 1.20, "cornetto": 1.50}
prezzi["spremuta"] = 3.00
totale = prezzi["caffè"] + prezzi.get("tè", 0)

Lettura, scrittura e verifica di una chiave richiedono in media tempo costante, indipendentemente dalla dimensione del dizionario. Dalla versione 3.7 di Python, inoltre, i dizionari mantengono l'ordine di inserimento come garanzia del linguaggio.

Il metodo get con un valore predefinito evita l'eccezione KeyError quando una chiave può mancare. Le chiavi devono essere oggetti immutabili e confrontabili in modo affidabile: stringhe, numeri, tuple di valori immutabili.

Insiemi: appartenenza e unicità

Un insieme contiene elementi unici, senza un ordine garantito, e risponde in tempo medio costante alla domanda «questo elemento c'è?».

email_note = {"a@esempio.it", "b@esempio.it"}
if nuova_email in email_note:
    print("già registrata")

Gli insiemi offrono anche le operazioni della teoria degli insiemi, molto utili per confrontare collezioni:

iscritti_corso_a & iscritti_corso_b   # intersezione: iscritti a entrambi
iscritti_corso_a - iscritti_corso_b   # differenza: solo al corso A

Rimuovere i duplicati da una lista diventa banale con set(lista), con l'avvertenza che l'ordine originale si perde; se serve mantenerlo, list(dict.fromkeys(lista)) è un'alternativa compatta.

Quale scegliere

Esigenza Struttura consigliata
Sequenza ordinata da scorrere o modificare Lista
Gruppo fisso di valori correlati Tupla o dataclass
Cercare un valore a partire da una chiave Dizionario
Verificare spesso l'appartenenza o eliminare duplicati Insieme
Aggiungere e togliere da entrambe le estremità collections.deque
Contare occorrenze collections.Counter
Raggruppare valori per chiave collections.defaultdict(list)

Il modulo collections

La libreria standard offre strutture specializzate che evitano di reinventare la ruota.

deque è una coda a doppia estremità: aggiungere e rimuovere all'inizio o alla fine costa tempo costante. È la scelta naturale per code di elaborazione o per tenere solo gli ultimi N elementi, grazie al parametro maxlen.

Counter conta le occorrenze in una riga:

from collections import Counter
parole = testo.lower().split()
piu_frequenti = Counter(parole).most_common(5)

defaultdict crea automaticamente un valore iniziale per le chiavi mancanti, eliminando i controlli ripetitivi quando si raggruppano dati:

from collections import defaultdict
ordini_per_cliente = defaultdict(list)
for ordine in ordini:
    ordini_per_cliente[ordine.cliente].append(ordine)

Un esempio concreto di scelta

Devi confrontare due elenchi di codici prodotto, uno del magazzino e uno del catalogo online, ciascuno con cinquantamila voci, per trovare i prodotti presenti solo in magazzino. Con due liste e un ciclo con in, il programma esegue un numero di confronti nell'ordine dei miliardi. Convertendo il catalogo in un insieme, ogni verifica diventa immediata e il risultato arriva in una frazione di secondo. Il codice cambia di una riga; le prestazioni cambiano di ordini di grandezza.

In sintesi

  • La scelta della struttura dati incide su prestazioni e leggibilità più di quanto sembri.
  • Le liste sono ottime per sequenze, ma la verifica di appartenenza costa O(n).
  • Dizionari e insiemi offrono accesso e ricerca in tempo medio costante.
  • Le tuple rappresentano gruppi fissi di valori e possono essere chiavi di un dizionario.
  • Il modulo collections offre deque, Counter e defaultdict per esigenze comuni.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica e in psicologia, unisce l'analisi tecnica alla comprensione del fattore umano.

Leggi anche