Modulo collections di Python
Scopri il modulo collections di Python: Counter, defaultdict, namedtuple, deque, OrderedDict e ChainMap con esempi pratici e quando usarli.
Il modulo integrato collections di Python fornisce tipi di contenitore specializzati che estendono o sostituiscono le strutture standard list, dict e tuple. Ogni tipo risolve un problema specifico che altrimenti richiederebbe diverse righe di codice aggiuntivo.
Questo capitolo tratta tutti e sei i tipi più utilizzati: Counter, defaultdict, namedtuple, deque, OrderedDict e ChainMap. Per ognuno vedrai quale problema risolve, come crearlo e utilizzarlo, e le insidie da evitare.
Non è necessaria nessuna installazione — collections è incluso in ogni installazione di Python 3:
from collections import Counter, defaultdict, namedtuple, deque, OrderedDict, ChainMapCounter
Counter è una sottoclasse di dict progettata per contare oggetti hashable. Gli si fornisce un iterabile (o una stringa, o argomenti keyword) e restituisce un oggetto simile a un dizionario in cui le chiavi sono gli elementi e i valori sono i rispettivi conteggi.
Creare un Counter
from collections import Counter
# From a list
word_list = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
c = Counter(word_list)
print(c)
# Output: Counter({'apple': 3, 'banana': 2, 'cherry': 1})Le chiavi mancanti restituiscono 0 invece di sollevare un KeyError:
print(c['apple']) # 3
print(c['mango']) # 0 — no KeyErrorElementi più frequenti
most_common(n) restituisce gli n elementi con il conteggio più alto come lista di tuple (elemento, conteggio), ordinate dalla più alla meno frequente:
print(c.most_common(2))
# Output: [('apple', 3), ('banana', 2)]Ometti n per ottenere tutti gli elementi ordinati per frequenza.
Aritmetica con Counter
I Counter supportano addizione, sottrazione, intersezione e unione:
a = Counter(['a', 'a', 'b']) # Counter({'a': 2, 'b': 1})
b = Counter(['a', 'b', 'b', 'c']) # Counter({'b': 2, 'a': 1, 'c': 1})
print(a + b) # Counter({'a': 3, 'b': 3, 'c': 1})
print(a - b) # Counter({'a': 1}) — only positive counts kept
print(a & b) # Counter({'a': 1, 'b': 1}) — minimum of each count
print(a | b) # Counter({'a': 2, 'b': 2, 'c': 1}) — maximum of each countQuando usare Counter: conteggio di voti, frequenze delle parole, conteggio di caratteri, costruzione di istogrammi.
Insidia di Counter: la sottrazione mantiene solo i valori positivi
a - b scarta silenziosamente gli elementi in cui il risultato sarebbe zero o negativo. Se hai bisogno di mantenere i conteggi negativi, usa invece subtract():
a = Counter({'x': 2})
b = Counter({'x': 5})
a.subtract(b)
print(a) # Counter({'x': -3}) — negative count preserveddefaultdict
defaultdict è una sottoclasse di dict che chiama una funzione factory per fornire un valore predefinito ogni volta che si accede a una chiave che non esiste ancora. Questo elimina la necessità di clausole di guardia come if key not in d:.
Creare un defaultdict
Passa la factory come primo argomento:
from collections import defaultdict
dd = defaultdict(int) # default value: int() == 0
words = ['cat', 'dog', 'cat', 'bird', 'dog', 'cat']
for word in words:
dd[word] += 1 # no KeyError on first access
print(dict(dd))
# Output: {'cat': 3, 'dog': 2, 'bird': 1}Senza defaultdict sarebbe necessario usare dd[word] = dd.get(word, 0) + 1 oppure un Counter.
Raggruppare elementi con list come factory
groups = defaultdict(list)
data = [('fruit', 'apple'), ('veggie', 'carrot'), ('fruit', 'banana'), ('veggie', 'broccoli')]
for category, item in data:
groups[category].append(item)
print(dict(groups))
# Output: {'fruit': ['apple', 'banana'], 'veggie': ['carrot', 'broccoli']}Funzioni factory più comuni
| Factory | Valore predefinito | Uso tipico |
|---|---|---|
int | 0 | Conteggio |
float | 0.0 | Accumulo di somme |
list | [] | Raggruppamento di elementi |
set | set() | Raccolta di valori unici |
str | '' | Costruzione di stringhe |
dict | {} | Mappature annidate |
Puoi anche passare una lambda senza argomenti per un valore predefinito personalizzato: defaultdict(lambda: 'N/A').
Insidia di defaultdict: accedere a una chiave la crea
A differenza di dict.get(), un semplice accesso dd[key] su una chiave mancante inserisce quella chiave con il valore predefinito. Questo può sorprendere durante l'iterazione o il controllo dell'appartenenza:
dd = defaultdict(int)
print('foo' in dd) # False — key does not exist yet
_ = dd['foo'] # access inserts the key
print('foo' in dd) # True — key was silently createdUsa dd.get('foo') o 'foo' in dd quando vuoi verificare senza effetti collaterali.
Quando usare defaultdict: raggruppamento di dati, costruzione di liste di adiacenza per grafi, qualsiasi schema in cui si inizializza e poi si aggiorna.
namedtuple
namedtuple crea una nuova classe le cui istanze sono come tuple normali ma con campi denominati. Il risultato è immutabile, efficiente in termini di memoria (nessun __dict__ per istanza) e autodescrittivo.
Creare un namedtuple
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 7)
print(p) # Point(x=3, y=7)
print(p.x) # 3
print(p.y) # 7
print(p[0]) # 3 — index access still worksIl primo argomento di namedtuple() è il nome del tipo (usato nel repr). Il secondo argomento è un elenco di nomi di campi (o una stringa separata da spazi/virgole: 'x y').
Esempio pratico
Employee = namedtuple('Employee', ['name', 'department', 'salary'])
emp = Employee('Alice', 'Engineering', 95000)
print(emp.name, emp.department, emp.salary)
# Output: Alice Engineering 95000L'accesso per nome (emp.name) è molto più chiaro dell'accesso per posizione (row[0]) quando si leggono dati da file CSV o righe di database.
Metodi utili di namedtuple
# Convert to an ordered dictionary
print(p._asdict()) # {'x': 3, 'y': 7}
# Create a modified copy (namedtuples are immutable)
p2 = p._replace(x=10)
print(p2) # Point(x=10, y=7)
print(p) # Point(x=3, y=7) — original unchangednamedtuple vs dataclass
Python 3.7 ha introdotto dataclasses.dataclass come alternativa. Scegli namedtuple quando vuoi immutabilità e piena compatibilità con le tuple (unpacking, indicizzazione, hashing). Scegli dataclass quando hai bisogno di campi mutabili, factory predefinite o metodi.
Quando usare namedtuple: rappresentare record (righe di database, righe CSV, coppie di coordinate, colori RGB) dove immutabilità e basso consumo di memoria sono importanti.
deque
deque (coda a doppia estremità, pronunciato "deck") è una sequenza ottimizzata per append e pop O(1) da entrambe le estremità. Una lista normale garantisce O(1) per append e O(n) per insert(0, …); deque garantisce O(1) da entrambe le estremità.
Creare un deque
from collections import deque
d = deque([1, 2, 3])
print(d) # deque([1, 2, 3])Aggiungere e rimuovere elementi
d.append(4) # add to right
d.appendleft(0) # add to left
print(d) # deque([0, 1, 2, 3, 4])
d.pop() # remove from right → 4
d.popleft() # remove from left → 0
print(d) # deque([1, 2, 3])Rotazione di un deque
rotate(n) sposta gli elementi verso destra di n posizioni (negativo = sinistra):
d = deque([1, 2, 3])
d.rotate(1)
print(d) # deque([3, 1, 2])
d.rotate(-1)
print(d) # deque([1, 2, 3])deque limitato (finestra scorrevole / buffer FIFO)
Impostare maxlen limita la dimensione del deque. Quando vengono aggiunti nuovi elementi oltre il limite, gli elementi cadono automaticamente dall'estremità opposta — perfetto per conservare gli ultimi N eventi:
buffer = deque(maxlen=3)
for i in range(5):
buffer.append(i)
print(buffer) # deque([2, 3, 4], maxlen=3)Insidia di deque: accesso casuale lento O(n)
deque non supporta l'accesso casuale efficiente. d[500] è O(n), non O(1) come in una lista. Se indicizzi frequentemente per posizione, usa una lista. Usa deque solo quando hai bisogno di aggiunta e rimozione rapide da entrambe le estremità.
Quando usare deque: implementazione di code e stack, algoritmi a finestra scorrevole, ricerca in ampiezza (BFS), conservazione delle ultime N voci di log.
OrderedDict
Da Python 3.7, il dict normale mantiene l'ordine di inserimento. Allora perché usare OrderedDict?
Rimangono rilevanti due motivi:
move_to_end()— consente di riordinare in modo efficiente le chiavi verso l'inizio o la fine.- Uguaglianza — due istanze di
OrderedDictcon le stesse chiavi ma ordine di inserimento diverso sono considerate non uguali, a differenza dei dict normali.
Creare e riordinare un OrderedDict
from collections import OrderedDict
od = OrderedDict()
od['one'] = 1
od['two'] = 2
od['three'] = 3
print(list(od.keys())) # ['one', 'two', 'three']
od.move_to_end('one') # move 'one' to the end
print(list(od.keys())) # ['two', 'three', 'one']
od.move_to_end('three', last=False) # move 'three' to the front
print(list(od.keys())) # ['three', 'two', 'one']Uguaglianza sensibile all'ordine
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2) # False — different order
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2) # True — regular dicts ignore orderQuando usare OrderedDict: implementazioni di cache LRU (sposta la chiave usata di recente alla fine), qualsiasi algoritmo in cui l'ordine di inserimento deve far parte dell'uguaglianza.
ChainMap
ChainMap raggruppa più dizionari in un'unica vista logica. Le ricerche esaminano le mappe nell'ordine; le scritture e le eliminazioni influenzano sempre solo la prima mappa.
Utilizzo di base
from collections import ChainMap
defaults = {'color': 'blue', 'size': 'medium', 'theme': 'light'}
overrides = {'color': 'red', 'size': 'large'}
combined = ChainMap(overrides, defaults)
print(combined['color']) # 'red' — found in overrides first
print(combined['theme']) # 'light' — not in overrides, falls back to defaultsLe scritture vengono indirizzate solo alla prima mappa:
combined['font'] = 'serif'
print(overrides) # {'color': 'red', 'size': 'large', 'font': 'serif'}
print(defaults) # {'color': 'blue', 'size': 'medium', 'theme': 'light'} — unchangedSimulare scope di variabili con new_child()
base = ChainMap({'x': 1})
child = base.new_child({'x': 99, 'y': 2})
print(child['x']) # 99 — child scope shadows parent
print(child['y']) # 2
print(child.parents['x']) # 1 — access parent scope directlynew_child() restituisce un nuovo ChainMap con un dict vuoto preposto, il che è il modo in cui le regole di scoping di Python (locale → racchiudente → globale → built-in) sono modellate internamente.
Quando usare ChainMap: stratificazione della configurazione (override utente → impostazioni predefinite del progetto → impostazioni predefinite globali), implementazione di ambienti con scope, combinazione di argomenti CLI con variabili d'ambiente e file di configurazione.
Scegliere il tipo giusto
| Hai bisogno di… | Usa |
|---|---|
| Contare le occorrenze degli elementi | Counter |
Evitare KeyError con un valore predefinito | defaultdict |
| Rappresentare un record con campi denominati | namedtuple |
| Append/pop veloci da entrambe le estremità, o un buffer limitato | deque |
Uguaglianza dict sensibile all'ordine, o move_to_end() | OrderedDict |
| Unire più dict in un'unica vista senza copiare | ChainMap |
Per ulteriori informazioni sui tipi base che questi estendono, consulta Python Dictionaries, Python Lists e Python Tuples. Per gli helper basati su iteratori della libreria standard, consulta il Modulo itertools di Python.