itertools est un module de la bibliothèque standard qui expose des briques d’itération combinables. Son intérêt n’est pas de remplacer une boucle for par une fonction au nom obscur, mais de manipuler des flux de données sans jamais les charger entièrement en mémoire. Chaque fonction retourne un itérateur paresseux : rien n’est calculé tant qu’on ne consomme pas le résultat. C’est ce qui permet de chaîner des transformations sur des millions d’éléments avec une empreinte mémoire constante.
Voici les fonctions que j’utilise réellement, regroupées par usage, avec les pièges qui font perdre du temps.
Itérateurs infinis : count, cycle, repeat
Ces trois fonctions produisent des flux sans fin. Elles ne sont utiles qu’associées à une condition d’arrêt, sinon la boucle ne se termine jamais.
count(start, step) génère une suite arithmétique infinie. Pratique pour numéroter sans gérer un compteur manuel.
from itertools import count, islice
for i in count(10, 2):
if i > 20:
break
print(i) # 10, 12, 14, 16, 18, 20
cycle(iterable) répète un itérable indéfiniment. Typique pour alterner entre des ressources (round-robin sur des serveurs, des couleurs, des workers).
from itertools import cycle
couleurs = cycle(['rouge', 'vert', 'bleu'])
for _, couleur in zip(range(5), couleurs):
print(couleur) # rouge, vert, bleu, rouge, vert
repeat(elem, times) répète une valeur. Sans times, c’est infini. Sa principale utilité est de fournir un argument constant à map ou starmap.
from itertools import repeat
list(repeat(7, 3)) # [7, 7, 7]
list(map(pow, range(5), repeat(2))) # [0, 1, 4, 9, 16] — chaque base au carré
Le piège. count(), cycle() et repeat() sans times ne s’arrêtent jamais. Il faut toujours les borner avec islice, takewhile, un zip sur une séquence finie, ou un break. Un list(count()) fige le processus.
Découper et filtrer un flux
islice(iterable, start, stop, step) applique un découpage de type slice sur n’importe quel itérateur, y compris infini, sans le matérialiser en liste. C’est l’outil de référence pour borner un flux.
from itertools import islice, count
list(islice(count(), 5)) # [0, 1, 2, 3, 4]
list(islice(count(), 2, 8, 2)) # [2, 4, 6]
takewhile(pred, iterable) retourne les éléments tant que le prédicat est vrai, puis s’arrête au premier échec. dropwhile fait l’inverse : il saute les éléments tant que le prédicat est vrai, puis retourne tout le reste sans réévaluer.
from itertools import takewhile, dropwhile
data = [2, 3, 8, 1, 9, 4]
list(takewhile(lambda x: x < 5, data)) # [2, 3] — s'arrête au 8
list(dropwhile(lambda x: x < 5, data)) # [8, 1, 9, 4] — saute 2 et 3, garde le reste
filterfalse(pred, iterable) est le complément de filter : il garde les éléments pour lesquels le prédicat est faux. Plus lisible que filter(lambda x: not pred(x), ...).
from itertools import filterfalse
list(filterfalse(lambda x: x % 2, range(10))) # [0, 2, 4, 6, 8] — les pairs
compress(data, selectors) filtre data selon un second itérable de booléens. Utile quand le masque de sélection est calculé ailleurs, séparément des données.
from itertools import compress
noms = ['Alice', 'Bob', 'Carol', 'Dan']
actifs = [True, False, True, False]
list(compress(noms, actifs)) # ['Alice', 'Carol']
Combiner et transformer
chain(*iterables) enchaîne plusieurs itérables en une seule séquence, sans créer de liste intermédiaire. Sa variante chain.from_iterable aplatit un itérable d’itérables, idéale pour des flux paresseux.
from itertools import chain
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
list(chain.from_iterable([[1, 2], [3, 4]])) # [1, 2, 3, 4]
accumulate(iterable, func, initial) produit des résultats cumulés. Par défaut, c’est une somme cumulative, mais n’importe quelle fonction binaire convient (max, operator.mul, etc.). Le paramètre initial (Python 3.8+) fixe une valeur de départ.
from itertools import accumulate
import operator
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10] — somme cumulée
list(accumulate([1, 2, 3, 4], operator.mul)) # [1, 2, 6, 24] — produit cumulé
list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5] — maximum courant
starmap(func, iterable) applique une fonction à des arguments déjà groupés en tuples. C’est map quand les arguments sont pré-empaquetés : starmap(f, [(a, b)]) appelle f(a, b).
from itertools import starmap
points = [(3, 4), (6, 8), (5, 12)]
list(starmap(lambda x, y: (x**2 + y**2)**0.5, points)) # [5.0, 10.0, 13.0]
pairwise(iterable) (Python 3.10+) retourne les éléments par paires consécutives qui se chevauchent. Parfait pour calculer des différences ou comparer chaque élément au suivant.
from itertools import pairwise
list(pairwise([1, 2, 3, 4])) # [(1, 2), (2, 3), (3, 4)]
temps = [10, 13, 12, 18]
[b - a for a, b in pairwise(temps)] # [3, -1, 6] — variations successives
zip_longest(*iterables, fillvalue) fusionne plusieurs itérables comme zip, mais s’aligne sur le plus long en comblant les trous avec fillvalue au lieu de s’arrêter au plus court.
from itertools import zip_longest
list(zip_longest([1, 2, 3], ['a', 'b'], fillvalue='?'))
# [(1, 'a'), (2, 'b'), (3, '?')]
groupby : le piège du tri préalable
groupby(iterable, key) regroupe les éléments consécutifs partageant la même clé. Le mot important est consécutifs : groupby ne regroupe que des runs adjacents, il ne trie pas. Sur une entrée non triée par la clé, on obtient des groupes fragmentés.
from itertools import groupby
data = [('FR', 'Paris'), ('US', 'NYC'), ('FR', 'Lyon')]
# Mauvais : non trié par pays → 'FR' apparaît en deux groupes séparés
for pays, groupe in groupby(data, key=lambda x: x[0]):
print(pays, [v for _, v in groupe])
# FR ['Paris']
# US ['NYC']
# FR ['Lyon']
# Bon : trier d'abord par la même clé
data.sort(key=lambda x: x[0])
for pays, groupe in groupby(data, key=lambda x: x[0]):
print(pays, [v for _, v in groupe])
# FR ['Paris', 'Lyon']
# US ['NYC']
C’est l’erreur la plus fréquente sur cette fonction : oublier le sort avec la même clé que le groupby. Autre subtilité, l’objet groupe est un itérateur partagé : si on passe au groupe suivant sans avoir consommé le précédent, son contenu est perdu. La fonction key se prête bien à operator.itemgetter, plus rapide et lisible qu’une lambda.
tee : dupliquer un itérateur, avec prudence
tee(iterable, n) retourne n itérateurs indépendants à partir d’un seul. Il ne copie pas les données : les itérateurs partagent un buffer interne qui mémorise tout ce que le plus lent n’a pas encore consommé.
from itertools import tee
it = iter([1, 2, 3, 4])
a, b = tee(it, 2)
list(a) # [1, 2, 3, 4]
list(b) # [1, 2, 3, 4]
Deux pièges réels. D’abord, ne plus toucher à l’itérable source après tee : continuer à le consommer désynchronise les copies. Ensuite, si l’un des itérateurs prend beaucoup d’avance sur l’autre, le buffer interne grossit pour retenir les éléments en attente. Dupliquer un flux puis consommer entièrement la première copie avant la seconde revient à tout garder en mémoire, ce qui annule l’avantage de la paresse. tee est efficace seulement si les copies avancent à peu près au même rythme.
Combinatoire : product, permutations, combinations
Ces fonctions génèrent des arrangements. Elles restent paresseuses, mais attention : le nombre de résultats explose vite (factoriel ou exponentiel).
product(*iterables, repeat) calcule le produit cartésien. Il remplace les boucles for imbriquées.
from itertools import product
list(product([1, 2], ['a', 'b'])) # [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
list(product([0, 1], repeat=3)) # toutes les combinaisons binaires sur 3 bits
permutations(iterable, r) génère tous les arrangements ordonnés de longueur r (l’ordre compte). combinations(iterable, r) génère les sous-ensembles non ordonnés de longueur r (l’ordre ne compte pas). combinations_with_replacement autorise en plus la répétition d’un même élément.
from itertools import permutations, combinations, combinations_with_replacement
list(permutations('ABC', 2)) # AB AC BA BC CA CB — 6 arrangements
list(combinations('ABC', 2)) # AB AC BC — 3 combinaisons
list(combinations_with_replacement('ABC', 2)) # AA AB AC BB BC CC — 6 avec répétition
Le piège. permutations(range(10)) produit 3 628 800 tuples. Ne jamais envelopper ces fonctions dans un list() sans borner la taille de l’entrée, sous peine de saturer la mémoire. Itérer directement avec un break ou un islice quand on ne cherche qu’un échantillon.
Récapitulatif
| Fonction | Rôle | À retenir |
|---|---|---|
count / cycle / repeat | flux infinis | borner avec islice ou takewhile |
islice | slice paresseux | fonctionne sur un itérateur infini |
takewhile / dropwhile | couper selon un prédicat | s’arrête / saute au premier basculement |
filterfalse / compress | filtrage | complément de filter / masque externe |
chain | concaténer des itérables | from_iterable pour aplatir |
accumulate | résultats cumulés | func et initial personnalisables |
starmap | map sur des tuples d’arguments | f((a, b)) → f(a, b) |
pairwise | paires consécutives | Python 3.10+, idéal pour les écarts |
zip_longest | zip sans troncature | comble avec fillvalue |
groupby | regrouper des runs | trier d’abord par la clé |
tee | dupliquer un itérateur | coûteux mémoire si désynchronisé |
product / permutations / combinations | combinatoire | la sortie explose, borner l’entrée |
itertools n’apporte pas de fonctionnalité impossible à écrire à la main. Il apporte des primitives en C, testées, qui composent entre elles et qui préservent la paresse de bout en bout. Là où une compréhension de liste matérialise tout, un pipeline itertools traite un élément à la fois. Sur de gros volumes, c’est la différence entre un script qui tient en mémoire et un qui sature. Pour les structures de données complémentaires, voir le module collections.
