Raggruppare Dati con le Tuple Python
Impara a raggruppare dati con le tuple Python: chiavi dict, raggruppamento multi-chiave, itertools.groupby, namedtuple, Counter e zip.
Le tuple sono ideali per raggruppare dati in Python. Poiché una tupla è immutabile e hashable, può fungere da chiave di dizionario — cosa che una lista non può mai fare. Questo rende le tuple la scelta naturale ogni volta che è necessario raggruppare record in base a una combinazione di campi, tracciare coordinate multidimensionali o contare eventi composti.
Questo capitolo tratta quattro pattern pratici di raggruppamento:
- Tupla come chiave di dizionario — raggruppamento per campo singolo e multi-campo
itertools.groupbycon le tuple — raggruppamento in streaming su sequenze ordinatecollections.namedtuple— aggiungere nomi ai record raggruppaticollections.Countercon le tuple — contare eventi composti
Capitoli correlati: Python Tuples · Access Tuples · Loop Tuples · Python Dictionaries · Python Lists Group
Perché le Tuple Possono Essere Chiavi di Dizionario
Python richiede che le chiavi di dizionario siano hashable — il loro valore non deve mai cambiare dopo che la chiave è stata memorizzata. Le tuple soddisfano questo requisito perché sono immutabili. Le liste non lo soddisfano e generano un TypeError quando si tenta di usarle come chiavi.
# A tuple can be a dictionary key
coordinates = {}
coordinates[(10, 20)] = "warehouse A"
coordinates[(30, 40)] = "warehouse B"
print(coordinates[(10, 20)]) # warehouse A
# A list cannot be a dictionary key
try:
d = {[10, 20]: "warehouse A"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Un'importante insidia da tenere a mente: una tupla che contiene un elemento mutabile (come una lista) è anch'essa non hashable e non può essere usata come chiave:
try:
d = {(1, [2, 3]): "value"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Assicurati che le chiavi tuple siano composte interamente da valori immutabili — stringhe, numeri, boolean o altre tuple.
Raggruppamento per un Singolo Campo di Tupla
Il caso d'uso più semplice consiste nel destrutturare una sequenza di tuple e raggruppare per un elemento. Usa collections.defaultdict(list) per evitare il codice ridondante necessario per verificare se una chiave esiste già.
from collections import defaultdict
employees = [
("Alice", "Engineering"),
("Bob", "Marketing"),
("Carol", "Engineering"),
("Dave", "Marketing"),
("Eve", "Engineering"),
]
by_dept = defaultdict(list)
for name, dept in employees:
by_dept[dept].append(name)
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: ['Alice', 'Carol', 'Eve']
# Marketing: ['Bob', 'Dave']defaultdict(list) crea automaticamente una lista vuota la prima volta che viene incontrata una nuova chiave dept, quindi non è necessario nessun controllo del tipo if dept not in by_dept.
Raggruppamento Multi-Chiave con una Chiave Tupla
Il vero potere delle chiavi tuple emerge quando è necessario raggruppare per più di un campo contemporaneamente. Combina i campi in una tupla e utilizza quella tupla come chiave del dizionario.
from collections import defaultdict
records = [
("Alice", "Engineering", "Senior"),
("Bob", "Marketing", "Junior"),
("Carol", "Engineering", "Junior"),
("Dave", "Marketing", "Senior"),
("Eve", "Engineering", "Senior"),
]
# Group by (department, level) — a two-field composite key
grouped = defaultdict(list)
for name, dept, level in records:
grouped[(dept, level)].append(name)
for (dept, level), names in sorted(grouped.items()):
print(f"{dept} / {level}: {names}")
# Engineering / Junior: ['Carol']
# Engineering / Senior: ['Alice', 'Eve']
# Marketing / Junior: ['Bob']
# Marketing / Senior: ['Dave']Poiché (dept, level) è essa stessa una tupla, è hashable e può fungere da chiave dict indipendentemente da quanti campi contenga. Destrutturare la chiave con for (dept, level), names in ... mantiene il codice leggibile.
Raggruppamento di griglie e coordinate
Il raggruppamento multi-chiave con tuple gestisce naturalmente anche i dati spaziali:
points = [(0, 0), (1, 2), (0, 1), (1, 3), (2, 4)]
from collections import defaultdict
by_x = defaultdict(list)
for x, y in points:
by_x[x].append(y)
for x, ys in sorted(by_x.items()):
print(f"x={x}: y-values={ys}")
# x=0: y-values=[0, 1]
# x=1: y-values=[2, 3]
# x=2: y-values=[4]Raggruppamento con itertools.groupby
itertools.groupby raggruppa elementi consecutivi che condividono la stessa chiave. È efficiente in termini di memoria perché è lazy — non carica tutti i gruppi in memoria contemporaneamente. Il compromesso è che l'input deve essere ordinato per la stessa chiave prima di essere passato a groupby, altrimenti si ottengono più gruppi parziali per la stessa chiave invece di uno solo.
from itertools import groupby
sales = [
("East", "Q1", 1200),
("East", "Q2", 1500),
("West", "Q1", 900),
("West", "Q2", 1100),
("East", "Q3", 1800),
]
# Sort by region (index 0) before grouping
sales_sorted = sorted(sales, key=lambda t: t[0])
for region, group in groupby(sales_sorted, key=lambda t: t[0]):
items = list(group)
total = sum(q[2] for q in items)
print(f"{region}: total={total}, quarters={[q[1] for q in items]}")
# East: total=4500, quarters=['Q1', 'Q2', 'Q3']
# West: total=2000, quarters=['Q1', 'Q2']Due cose da ricordare quando si usa groupby con le tuple:
- Ordina prima. Senza ordinamento, ogni nuova sequenza dello stesso valore chiave crea un gruppo separato.
- Consuma l'iteratore del gruppo immediatamente. L'iteratore interno
groupviene esaurito quando il ciclo esterno passa alla chiave successiva. Chiama semprelist(group)nel corpo del ciclo prima di usarlo altrove.
Quando groupby è preferibile a defaultdict
Usa groupby quando elabori una sequenza lunga già ordinata e non vuoi caricare l'intero risultato raggruppato in memoria. Per il raggruppamento generico senza garanzia di ordinamento, defaultdict(list) è più semplice e affidabile.
Raggruppamento con collections.namedtuple
namedtuple ti permette di assegnare nomi ai campi delle tuple, rendendo i dati raggruppati auto-documentanti. Una volta definito il tipo namedtuple, le istanze si comportano esattamente come le tuple ordinarie — sono immutabili, hashable e iterabili — ma i campi sono accessibili per nome oltre che per indice.
from collections import namedtuple, defaultdict
Employee = namedtuple("Employee", ["name", "department", "salary"])
employees = [
Employee("Alice", "Engineering", 95000),
Employee("Bob", "Marketing", 72000),
Employee("Carol", "Engineering", 88000),
Employee("Dave", "Marketing", 68000),
Employee("Eve", "Engineering", 102000),
]
by_dept = defaultdict(list)
for emp in employees:
by_dept[emp.department].append(emp)
for dept, members in sorted(by_dept.items()):
avg_salary = sum(e.salary for e in members) / len(members)
print(f"{dept}: {[e.name for e in members]}, avg salary={avg_salary:.0f}")
# Engineering: ['Alice', 'Carol', 'Eve'], avg salary=95000
# Marketing: ['Bob', 'Dave'], avg salary=70000Nota che emp.department e emp.salary sono più leggibili di emp[1] e emp[2]. L'approccio con namedtuple è particolarmente utile quando la tupla ha molti campi e l'indicizzazione posizionale diventa difficile da seguire.
Contare Eventi Composti con Counter
collections.Counter conta oggetti hashable. Quando l'"oggetto" che vuoi contare è una combinazione di valori, racchiudi quei valori in una tupla e passa la sequenza di tuple a Counter.
from collections import Counter
log = [
("GET", 200),
("POST", 201),
("GET", 200),
("GET", 404),
("POST", 500),
("GET", 200),
("DELETE", 204),
]
counts = Counter(log)
for entry, n in counts.most_common():
method, status = entry
print(f"{method} {status}: {n} times")
# GET 200: 3 times
# POST 201: 1 times
# GET 404: 1 times
# POST 500: 1 times
# DELETE 204: 1 timesCounter utilizza la tupla come chiave hash internamente, quindi ogni combinazione unica (method, status) viene tracciata separatamente senza alcun codice di raggruppamento manuale.
Costruire Gruppi di Tuple con zip
zip accoppia elementi di due o più sequenze in tuple. Questo è un modo naturale per assemblare record raggruppati da liste parallele prima di applicare un'operazione di raggruppamento.
from collections import defaultdict
names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 92]
departments = ["Engineering", "Marketing", "Engineering"]
# Pair the three sequences into tuples
records = list(zip(names, scores, departments))
print(records)
# [('Alice', 95, 'Engineering'), ('Bob', 87, 'Marketing'), ('Carol', 92, 'Engineering')]
# Now group by department
by_dept = defaultdict(list)
for name, score, dept in records:
by_dept[dept].append((name, score))
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: [('Alice', 95), ('Carol', 92)]
# Marketing: [('Bob', 87)]Scegliere lo Strumento di Raggruppamento Giusto
| Obiettivo | Strumento migliore |
|---|---|
| Raggruppare per un campo da una lista di tuple | defaultdict(list) |
| Raggruppare per due o più campi contemporaneamente | defaultdict(list) con chiave tupla |
| Raggruppamento in streaming su una sequenza pre-ordinata | itertools.groupby |
| Aggiungere nomi ai campi dei record raggruppati | collections.namedtuple |
| Contare le occorrenze di eventi composti | collections.Counter |
| Assemblare liste parallele in tuple raggruppate | zip |
Errori Comuni
Dimenticare di ordinare prima di groupby. itertools.groupby unisce solo chiavi identiche consecutive. Se la stessa chiave appare in più posizioni non consecutive, ogni sequenza diventa un gruppo separato. Ordina sempre per la stessa funzione chiave prima di chiamare groupby.
Usare un valore mutabile all'interno di una chiave tupla. Una tupla contenente una lista non è hashable e genera un TypeError quando viene usata come chiave dict. Mantieni le chiavi tuple composte da stringhe, numeri, boolean o tuple annidate.
Consumare l'iteratore di groupby più di una volta. L'iteratore del sotto-gruppo di groupby viene esaurito non appena il ciclo esterno avanza. Chiama list(group) nel corpo del ciclo se hai bisogno di iterare il gruppo più di una volta.
Trattare l'output di defaultdict come un dict ordinario. Un defaultdict crea automaticamente nuove chiavi quando leggi una chiave mancante, il che può popolare silenziosamente il dict con liste vuote. Se devi verificare l'esistenza di una chiave senza creare nuove voci, converti prima in un dict ordinario: dict(grouped).
Argomenti Correlati
- Python Tuples — creazione, indicizzazione e slicing delle tuple
- Access Tuples — indicizzazione, slicing e verifica di appartenenza
- Loop Tuples — iterare su una tupla con
forewhile - Unpack Tuples — assegnare elementi di tuple a variabili in un'unica riga
- Update Tuples — soluzioni alternative per modificare tuple immutabili
- Join Tuples — concatenazione e il costruttore
tuple() - Tuple Methods —
count()eindex()in dettaglio - Python Dictionaries — l'archivio chiave-valore che rende possibile il raggruppamento con chiavi tuple
- Python Lists Group — raggruppamento di liste con
defaultdict,groupbye dict comprehension - Python Collections Module —
defaultdict,Counter,namedtuplee altro