Modulo itertools di Python
Padroneggia il modulo itertools di Python: iteratori infiniti, combinatoria, raggruppamento, concatenazione e filtraggio — con esempi chiari ed eseguibili.
Il modulo itertools di Python è un toolkit della libreria standard composto da blocchi costruttivi veloci e ad alta efficienza di memoria per lavorare con gli iteratori. Ogni funzione di itertools restituisce un iteratore — produce valori su richiesta invece di costruire una lista in memoria — rendendo il modulo ideale per grandi dataset, sequenze infinite e pipeline di dati componibili.
Questo capitolo tratta tutte e tre le categorie di funzioni di itertools: iteratori infiniti (count, cycle, repeat), iteratori combinatorici (product, permutations, combinations, combinations_with_replacement) e iteratori terminanti (chain, islice, groupby, compress, filterfalse, takewhile, dropwhile, starmap, zip_longest, accumulate, pairwise).
Non è necessaria alcuna installazione — itertools è incluso in ogni installazione di Python 3:
import itertoolsPerché usare itertools?
Considera il caso di leggere i primi 10 multipli di un numero. Senza itertools hai bisogno di una lista o di un contatore manuale. Con itertools.count e itertools.islice l'intento è immediatamente chiaro e l'utilizzo della memoria rimane costante:
import itertools
multiples = itertools.islice(itertools.count(0, 7), 10)
print(list(multiples))
# [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]La filosofia di itertools: costruire un pezzo piccolo e corretto, poi comporlo con altri. Concatenare due funzioni di itertools è più veloce e meno soggetto a errori rispetto a scrivere il ciclo equivalente a mano.
Iteratori infiniti
Questi iteratori producono valori indefinitamente. Abbinali sempre a islice, a un for … break, o a un altro meccanismo di terminazione per evitare un ciclo infinito.
count(start=0, step=1)
count produce una sequenza di numeri equispaziati. È essenzialmente range senza limite superiore e con supporto per float e passi negativi.
import itertools
# Integer counter
for n in itertools.islice(itertools.count(10), 5):
print(n, end=' ')
# 10 11 12 13 14
print()
# Float step
for n in itertools.islice(itertools.count(0.0, 0.5), 5):
print(n, end=' ')
# 0.0 0.5 1.0 1.5 2.0
print()
# Countdown
for n in itertools.islice(itertools.count(100, -10), 5):
print(n, end=' ')
# 100 90 80 70 60count è utile quando hai bisogno di numerare gli elementi di un iterabile senza sapere in anticipo quanti ce ne sono — l'idioma di enumerate ma con un inizio e un passo personalizzati.
cycle(iterable)
cycle ripete gli elementi di qualsiasi iterabile indefinitamente.
import itertools
colours = itertools.cycle(['red', 'green', 'blue'])
for i, colour in enumerate(colours):
if i == 7:
break
print(colour, end=' ')
# red green blue red green blue redUtilizzo pratico — assegnare elementi a squadre in modalità round-robin:
import itertools
teams = itertools.cycle(['Alpha', 'Beta', 'Gamma'])
players = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
assignments = {player: team for player, team in zip(players, teams)}
print(assignments)
# {'Alice': 'Alpha', 'Bob': 'Beta', 'Carol': 'Gamma', 'Dave': 'Alpha', 'Eve': 'Beta'}repeat(object, times=None)
repeat restituisce lo stesso object times volte (o all'infinito se times è omesso).
import itertools
# Finite repeat
print(list(itertools.repeat('hello', 3)))
# ['hello', 'hello', 'hello']
# Used as a fixed argument supplier in map()
squares = list(map(pow, range(1, 6), itertools.repeat(2)))
print(squares)
# [1, 4, 9, 16, 25]Il pattern map(pow, range(1, 6), repeat(2)) è un idioma comune per fornire un secondo argomento costante a una funzione con due argomenti.
Iteratori combinatorici
Questi iteratori producono tutte le combinazioni, permutazioni o prodotti cartesiani di un iterabile in input. Sono essenziali per ricerche a forza bruta, generazione di casi di test e problemi di combinatoria.
product(*iterables, repeat=1)
product calcola il prodotto cartesiano — tutte le combinazioni ordinate in cui un elemento è prelevato da ciascun iterabile. È equivalente a cicli for annidati.
import itertools
suits = ['Hearts', 'Diamonds']
ranks = ['A', 'K', 'Q']
deck = list(itertools.product(suits, ranks))
print(deck)
# [('Hearts', 'A'), ('Hearts', 'K'), ('Hearts', 'Q'),
# ('Diamonds', 'A'), ('Diamonds', 'K'), ('Diamonds', 'Q')]Usa repeat per calcolare il prodotto di un iterabile con se stesso più volte:
import itertools
# All 2-digit binary numbers
binary_pairs = list(itertools.product([0, 1], repeat=2))
print(binary_pairs)
# [(0, 0), (0, 1), (1, 0), (1, 1)]Attenzione: product materializza gli iterabili in input nella memoria (per consentire più passaggi), quindi non passare iteratori enormi come input.
permutations(iterable, r=None)
permutations produce tutte le disposizioni ordinate di r elementi presi dall'input. Quando r è omesso, vengono usati tutti gli elementi.
import itertools
# All orderings of 3 letters
perms = list(itertools.permutations('ABC'))
print(perms)
# [('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'),
# ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]
print(len(perms)) # 6 (3! = 6)
# 2-element permutations
perms2 = list(itertools.permutations('ABC', 2))
print(perms2)
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
print(len(perms2)) # 6 (3 * 2 = 6)L'ordine conta nelle permutazioni — ('A', 'B') e ('B', 'A') sono risultati distinti.
combinations(iterable, r)
combinations produce tutte le selezioni non ordinate di r elementi. A differenza di permutations, l'ordine non conta — ogni sottoinsieme appare una sola volta.
import itertools
# All 2-element subsets of [1, 2, 3, 4]
combos = list(itertools.combinations([1, 2, 3, 4], 2))
print(combos)
# [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
print(len(combos)) # 6 (C(4,2) = 6)Un caso d'uso comune — verificare tutte le coppie di elementi per una proprietà:
import itertools
words = ['bat', 'tab', 'cat', 'tac']
anagram_pairs = [
(a, b) for a, b in itertools.combinations(words, 2)
if sorted(a) == sorted(b)
]
print(anagram_pairs)
# [('bat', 'tab'), ('cat', 'tac')]combinations_with_replacement(iterable, r)
Come combinations, ma consente a ciascun elemento di comparire più di una volta in una selezione.
import itertools
# All 2-element combinations with repetition from [1, 2, 3]
combos = list(itertools.combinations_with_replacement([1, 2, 3], 2))
print(combos)
# [(1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 3)]Questo è utile per generare tutti i possibili lanci di dadi, sequenze di lanci di moneta o scelte di caratteri per password.
Funzioni combinatoriche a colpo d'occhio
| Funzione | L'ordine conta? | Ripetizioni ammesse? | Conteggio (n=4, r=2) |
|---|---|---|---|
product | Sì | Sì | n^r = 16 |
permutations | Sì | No | n!/(n-r)! = 12 |
combinations | No | No | C(n,r) = 6 |
combinations_with_replacement | No | Sì | C(n+r-1,r) = 10 |
Iteratori terminanti
Gli iteratori terminanti elaborano un input finito e si fermano quando quell'input è esaurito.
chain(*iterables)
chain tratta diversi iterabili come un'unica sequenza continua senza costruire una nuova lista.
import itertools
a = [1, 2, 3]
b = (4, 5)
c = range(6, 9)
combined = list(itertools.chain(a, b, c))
print(combined)
# [1, 2, 3, 4, 5, 6, 7, 8]chain.from_iterable accetta un singolo iterabile di iterabili — utile quando non si conosce in anticipo il numero di sequenze:
import itertools
nested = [[1, 2], [3, 4], [5, 6]]
flat = list(itertools.chain.from_iterable(nested))
print(flat)
# [1, 2, 3, 4, 5, 6]Questa è un'alternativa veloce e ad alta efficienza di memoria rispetto a [item for sublist in nested for item in sublist].
islice(iterable, stop) / islice(iterable, start, stop, step=1)
islice suddivide qualsiasi iteratore — compresi quelli infiniti — senza materializzarlo. Gli argomenti rispecchiano la notazione slice di Python ma accettano solo interi non negativi.
import itertools
# First 5 elements
print(list(itertools.islice(range(100), 5)))
# [0, 1, 2, 3, 4]
# Elements 10–14 (start inclusive, stop exclusive)
print(list(itertools.islice(range(100), 10, 15)))
# [10, 11, 12, 13, 14]
# Every other element from position 0 to 10
print(list(itertools.islice(range(20), 0, 10, 2)))
# [0, 2, 4, 6, 8]islice non supporta indici negativi né passi negativi (a differenza del normale slicing delle liste).
groupby(iterable, key=None)
groupby raggruppa elementi consecutivi che condividono lo stesso valore di chiave. Restituisce coppie (key, group_iterator).
import itertools
data = [
('fruit', 'apple'),
('fruit', 'banana'),
('veggie', 'carrot'),
('veggie', 'broccoli'),
('fruit', 'cherry'),
]
for category, group in itertools.groupby(data, key=lambda x: x[0]):
items = [item[1] for item in group]
print(f'{category}: {items}')
# fruit: ['apple', 'banana']
# veggie: ['carrot', 'broccoli']
# fruit: ['cherry']Attenzione critica: groupby raggruppa solo elementi consecutivi uguali. Se i tuoi dati non sono pre-ordinati per chiave, elementi simili in posizioni diverse formano gruppi separati (come mostrato sopra — 'cherry' avvia un nuovo gruppo 'fruit' invece di unirsi al primo). Ordina sempre per chiave prima di chiamare groupby:
import itertools
data = [
('fruit', 'apple'),
('veggie', 'carrot'),
('fruit', 'banana'),
('veggie', 'broccoli'),
('fruit', 'cherry'),
]
# Sort first, then group
sorted_data = sorted(data, key=lambda x: x[0])
for category, group in itertools.groupby(sorted_data, key=lambda x: x[0]):
items = [item[1] for item in group]
print(f'{category}: {items}')
# fruit: ['apple', 'banana', 'cherry']
# veggie: ['carrot', 'broccoli']Nota anche che l'iteratore del gruppo diventa non valido una volta avanzato alla chiave successiva — consuma ogni gruppo prima di chiamare next() sull'iteratore esterno.
compress(data, selectors)
compress filtra data mantenendo solo gli elementi il cui corrispondente valore selector è truthy.
import itertools
names = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve']
active = [True, False, True, True, False]
result = list(itertools.compress(names, active))
print(result)
# ['Alice', 'Carol', 'Dave']compress è equivalente a [d for d, s in zip(data, selectors) if s] ma è più veloce ed evita la lista intermedia.
filterfalse(predicate, iterable)
filterfalse è il complemento del built-in filter — restituisce gli elementi per cui il predicato è False.
import itertools
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Keep only odd numbers (those that fail the even test)
odds = list(itertools.filterfalse(lambda x: x % 2 == 0, numbers))
print(odds)
# [1, 3, 5, 7, 9]takewhile(predicate, iterable)
takewhile restituisce elementi finché il predicato è True, poi si ferma immediatamente — anche se elementi successivi soddisferebbero il predicato.
import itertools
data = [2, 4, 6, 3, 8, 10]
# Stop as soon as an odd number appears
evens_from_start = list(itertools.takewhile(lambda x: x % 2 == 0, data))
print(evens_from_start)
# [2, 4, 6]dropwhile(predicate, iterable)
dropwhile è lo specchio di takewhile: salta gli elementi mentre il predicato è True, poi restituisce tutti gli elementi rimanenti (inclusi quelli per cui il predicato sarebbe di nuovo True).
import itertools
data = [2, 4, 6, 3, 8, 10]
# Drop leading even numbers, yield everything from the first odd onward
result = list(itertools.dropwhile(lambda x: x % 2 == 0, data))
print(result)
# [3, 8, 10]takewhile e dropwhile sono utili per elaborare file di log o stream in cui si vuole saltare una sezione di intestazione o fermarsi a una riga sentinella.
starmap(function, iterable)
starmap applica una funzione a ciascun elemento di un iterabile, spacchettando l'elemento come argomenti posizionali. È l'equivalente di map ma per iterabili di tuple.
import itertools
pairs = [(2, 3), (4, 2), (10, 3)]
results = list(itertools.starmap(pow, pairs))
print(results)
# [8, 16, 1000]Confronta con map(pow, [2, 4, 10], [3, 2, 3]) — starmap funziona quando i tuoi argomenti sono già raggruppati come tuple.
zip_longest(*iterables, fillvalue=None)
Il built-in zip si ferma all'iterabile più corto. zip_longest riempie gli iterabili più corti con fillvalue in modo che tutti gli iterabili vengano consumati completamente.
import itertools
a = [1, 2, 3]
b = ['a', 'b', 'c', 'd', 'e']
print(list(zip(a, b)))
# [(1, 'a'), (2, 'b'), (3, 'c')] — b's 'd' and 'e' are lost
print(list(itertools.zip_longest(a, b, fillvalue=0)))
# [(1, 'a'), (2, 'b'), (3, 'c'), (0, 'd'), (0, 'e')]accumulate(iterable, func=operator.add, *, initial=None)
accumulate calcola totali progressivi (o qualsiasi altra aggregazione progressiva). Per impostazione predefinita somma, ma è possibile passare qualsiasi funzione con due argomenti.
import itertools
import operator
numbers = [1, 2, 3, 4, 5]
# Running sum (default)
print(list(itertools.accumulate(numbers)))
# [1, 3, 6, 10, 15]
# Running product
print(list(itertools.accumulate(numbers, operator.mul)))
# [1, 2, 6, 24, 120]
# Running maximum
data = [3, 1, 4, 1, 5, 9, 2, 6]
print(list(itertools.accumulate(data, max)))
# [3, 3, 4, 4, 5, 9, 9, 9]Il parametro initial (Python 3.8+) antepone un valore seme prima del primo elemento:
import itertools
print(list(itertools.accumulate([1, 2, 3], initial=100)))
# [100, 101, 103, 106]pairwise(iterable)
pairwise (Python 3.10+) restituisce coppie sovrapposte consecutive dall'iterabile.
import itertools
data = [1, 2, 3, 4, 5]
print(list(itertools.pairwise(data)))
# [(1, 2), (2, 3), (3, 4), (4, 5)]Questo è utile per calcolare differenze tra valori consecutivi, o per logiche a finestra scorrevole dove la dimensione della finestra è esattamente 2:
import itertools
prices = [10.0, 12.5, 11.0, 13.5, 15.0]
changes = [b - a for a, b in itertools.pairwise(prices)]
print(changes)
# [2.5, -1.5, 2.5, 1.5]Prima di Python 3.10, l'equivalente era zip(data, data[1:]) (funziona per sequenze) o un approccio manuale basato su tee (funziona per iteratori arbitrari).
Comporre itertools in pipeline
Il vero potere di itertools emerge quando si combinano le funzioni. Poiché ogni funzione restituisce un iteratore, è possibile concatenarle senza alcuna lista intermedia.
Esempio: le 3 parole più frequenti in un testo
import itertools
import operator
text = "the quick brown fox jumps over the lazy dog the fox"
words = text.split()
# Sort words so groupby can collect identical words together
sorted_words = sorted(words)
# Count each word using groupby
word_counts = (
(key, sum(1 for _ in group))
for key, group in itertools.groupby(sorted_words)
)
# Sort by count descending, take the top 3
top3 = list(itertools.islice(
sorted(word_counts, key=operator.itemgetter(1), reverse=True),
3
))
print(top3)
# [('the', 3), ('fox', 2), ('brown', 1)]Esempio: suddividere un iterabile in blocchi di dimensione fissa
import itertools
def batched(iterable, n):
"""Yield successive n-sized tuples from iterable."""
it = iter(iterable)
while chunk := tuple(itertools.islice(it, n)):
yield chunk
data = range(10)
for batch in batched(data, 3):
print(batch)
# (0, 1, 2)
# (3, 4, 5)
# (6, 7, 8)
# (9,)Python 3.12 include itertools.batched come built-in, quindi puoi sostituire l'helper sopra con itertools.batched(data, 3) su Python moderno.
Riferimento rapido
| Categoria | Funzione | Cosa fa |
|---|---|---|
| Infinito | count(start, step) | Numeri equispaziati all'infinito |
| Infinito | cycle(iterable) | Ripete gli elementi dell'iterabile all'infinito |
| Infinito | repeat(obj, n) | Restituisce obj esattamente n volte (o all'infinito) |
| Combinatorico | product(*its, repeat) | Prodotto cartesiano |
| Combinatorico | permutations(it, r) | Disposizioni ordinate, senza ripetizioni |
| Combinatorico | combinations(it, r) | Sottoinsiemi non ordinati, senza ripetizioni |
| Combinatorico | combinations_with_replacement(it, r) | Sottoinsiemi non ordinati, con ripetizioni |
| Terminante | chain(*its) | Concatena iterabili |
| Terminante | chain.from_iterable(it) | Appiattisce un livello di annidamento |
| Terminante | islice(it, stop) | Suddivide un iteratore |
| Terminante | groupby(it, key) | Raggruppa elementi consecutivi con chiave uguale |
| Terminante | compress(data, sel) | Filtra tramite maschera boolean |
| Terminante | filterfalse(pred, it) | Mantiene gli elementi dove il predicato è False |
| Terminante | takewhile(pred, it) | Restituisce mentre il predicato è True, poi si ferma |
| Terminante | dropwhile(pred, it) | Salta mentre il predicato è True, poi restituisce |
| Terminante | starmap(func, it) | Map con spacchettamento degli argomenti |
| Terminante | zip_longest(*its, fill) | Zip, riempiendo gli iterabili più corti |
| Terminante | accumulate(it, func) | Aggregazione progressiva |
| Terminante | pairwise(it) | Coppie sovrapposte consecutive (3.10+) |
Per i concetti di valutazione lazy alla base di itertools, vedi Python Generators e Python Iterators. Per helper in stile funzionale che completano itertools, vedi Python Lambda Functions e il modulo collections di Python.