Corso Python / Conteggi e raggruppamenti con collections e itertools

Collections offre contenitori per conteggi e accumuli, mentre itertools combina flussi di iterazione. Scegli in base al significato del risultato, all’ordine dei dati e al consumo degli iteratori: contare due spese non equivale a sommarne gli importi.

Contare occorrenze o accumulare valori

Counter conta le occorrenze degli elementi ricevuti. Se gli passi le categorie delle spese, food con valore 2 significa due record, non due unità di costo. Un accumulo degli importi richiede invece di aggiungere il valore numerico di ogni record.

defaultdict(int) crea lo zero iniziale quando accedi con parentesi quadre a una chiave mancante. Questo rende naturale un’istruzione di accumulo come totals[category] += amount. Il parametro è la funzione int, senza chiamarla: passare 0 al posto della funzione non definisce una factory valida.

La lettura con get non attiva la stessa creazione: su una chiave mancante può restituire None senza inserire nulla, mentre l’accesso con parentesi quadre in defaultdict(int) inserisce zero. Se la sola consultazione non deve creare chiavi, scegli la forma coerente con quel requisito. La documentazione di collections descrive questi contenitori.

Eseguire conteggi, totali e sequenze collegate

Salva il programma completo come collections_demo.py, in UTF-8, in una cartella di esercizio. Esegui python collections_demo.py nel terminale con l’interprete del progetto selezionato. Fuori dall’ambiente virtuale su macOS o Ubuntu usa python3 quando è il comando scelto. Non occorrono pacchetti esterni.

from collections import Counter, defaultdict
from itertools import chain, groupby

rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
counts = Counter(category for category, _ in rows)
totals = defaultdict(int)
for category, amount in rows:
    totals[category] += amount
print(sorted(counts.items()))
print(sorted(totals.items()))
print(list(chain([1, 2], [3])))
for category, group in groupby(sorted(rows), key=lambda row: row[0]):
    print(category, sum(amount for _, amount in group))

Il risultato previsto è:

[('bus', 1), ('food', 2)]
[('bus', 2500), ('food', 11500)]
[1, 2, 3]
bus 2500
food 11500

Counter riceve solo le categorie tramite un’espressione generatrice. Il ciclo separato accumula invece gli importi: due food producono il conteggio 2 e il totale 11500. L’ordinamento rende stabile la presentazione delle coppie categoria–risultato.

chain([1, 2], [3]) percorre prima la prima sequenza e poi la seconda; non somma i valori né ordina gli elementi. List materializza il risultato [1, 2, 3]. La parte finale usa groupby dopo aver ordinato le tuple: in questi dati la categoria è il primo componente, quindi le categorie uguali diventano adiacenti.

Raggruppare soltanto chiavi consecutive con groupby

itertools.groupby crea un gruppo finché la chiave resta uguale lungo la sequenza. Non raccoglie automaticamente tutti i food sparsi nell’ingresso come una generica aggregazione SQL GROUP BY. Per una somma per categoria con questo strumento, porta prima insieme le chiavi uguali.

Salva il secondo esempio completo come groups.py ed esegui python groups.py, mantenendo il primo programma per il confronto:

from itertools import groupby
rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
def category(row):
    return row[0]
for label, data in [("original", rows), ("sorted", sorted(rows, key=category))]:
    print(label)
    for name, group in groupby(data, key=category):
        print(name, sum(amount for _, amount in group))
print(category(rows[0]) == (lambda row: row[0])(rows[0]))

Il risultato previsto è:

original
food 3500
bus 2500
food 8000
sorted
bus 2500
food 11500
True

Key è una funzione che ricava da ciascun elemento il valore da confrontare. Qui category(row) e lambda row: row[0] restituiscono lo stesso componente. Nel secondo passaggio sorted usa proprio quella chiave: anche quando le tuple contengono altri dati, l’ordinamento richiesto dal raggruppamento resta espresso chiaramente.

I gruppi condividono l’iterazione sottostante. Devi consumare il gruppo corrente, calcolando per esempio sum, prima di avanzare a quello successivo. Conservare soltanto gli oggetti iteratore per leggerli tutti dopo può lasciare gruppi vuoti; se ti occorrono in seguito, raccogli subito gli elementi o il risultato dell’aggregazione.

Questo comportamento segue il modello di iteratori e generatori consumabili. La documentazione di groupby specifica la dipendenza dall’ordine e dalla sorgente condivisa.

Scegliere in base a memoria e significato

Chain collega gli ingressi senza costruire in anticipo una nuova lista completa; raccoglierne poi tutti i risultati con list richiede comunque memoria per quella lista. Sorted, usato negli esempi, deve a sua volta raccogliere e ordinare l’ingresso: non rende l’intera procedura una pipeline senza memoria aggiuntiva.

islice permette di consumare un tratto di un iteratore. Se la sorgente è infinita, usa un limite finito o un’uscita esplicita invece di tentare list sull’intero flusso. Questi strumenti rendono più chiara l’intenzione, ma richiedono sempre di capire quanti elementi verranno richiesti.

  1. Nel primo programma togli soltanto sorted intorno a rows nella chiamata groupby. I conteggi e i totali precedenti restano uguali, ma food compare in due gruppi, da 3500 e 8000.
  2. Ripristina l’ordinamento e verifica un solo gruppo food con 11500.
  3. In una prova separata aggiungi un record food con importo 0: il conteggio sale a 3 mentre il totale resta 11500. Ripristina infine le righe originali.

Counter è appropriato per frequenze; defaultdict o un dizionario con una regola esplicita servono per somme e altri accumuli. Per rivedere chiavi, valori e accessi mancanti, la guida ai dizionari offre il contesto di base. Usa il contenitore che esprime il risultato desiderato, senza confondere un conteggio con una misura.