W3docs

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 itertools

Perché 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 60

count è 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 red

Utilizzo 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

FunzioneL'ordine conta?Ripetizioni ammesse?Conteggio (n=4, r=2)
productn^r = 16
permutationsNon!/(n-r)! = 12
combinationsNoNoC(n,r) = 6
combinations_with_replacementNoC(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

CategoriaFunzioneCosa fa
Infinitocount(start, step)Numeri equispaziati all'infinito
Infinitocycle(iterable)Ripete gli elementi dell'iterabile all'infinito
Infinitorepeat(obj, n)Restituisce obj esattamente n volte (o all'infinito)
Combinatoricoproduct(*its, repeat)Prodotto cartesiano
Combinatoricopermutations(it, r)Disposizioni ordinate, senza ripetizioni
Combinatoricocombinations(it, r)Sottoinsiemi non ordinati, senza ripetizioni
Combinatoricocombinations_with_replacement(it, r)Sottoinsiemi non ordinati, con ripetizioni
Terminantechain(*its)Concatena iterabili
Terminantechain.from_iterable(it)Appiattisce un livello di annidamento
Terminanteislice(it, stop)Suddivide un iteratore
Terminantegroupby(it, key)Raggruppa elementi consecutivi con chiave uguale
Terminantecompress(data, sel)Filtra tramite maschera boolean
Terminantefilterfalse(pred, it)Mantiene gli elementi dove il predicato è False
Terminantetakewhile(pred, it)Restituisce mentre il predicato è True, poi si ferma
Terminantedropwhile(pred, it)Salta mentre il predicato è True, poi restituisce
Terminantestarmap(func, it)Map con spacchettamento degli argomenti
Terminantezip_longest(*its, fill)Zip, riempiendo gli iterabili più corti
Terminanteaccumulate(it, func)Aggregazione progressiva
Terminantepairwise(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.

Esercizi

Pratica
Which itertools function would you use to stop consuming a generator the moment a condition becomes False?
Which itertools function would you use to stop consuming a generator the moment a condition becomes False?
Pratica
What is the critical requirement before calling itertools.groupby() if you want all matching elements to end up in the same group?
What is the critical requirement before calling itertools.groupby() if you want all matching elements to end up in the same group?
Pratica
Which itertools function produces the Cartesian product of two iterables?
Which itertools function produces the Cartesian product of two iterables?
Pratica
You call itertools.combinations('ABCD', 2). How many tuples does the result contain?
You call itertools.combinations('ABCD', 2). How many tuples does the result contain?
Was this page helpful?