Le module itertools

Itertools Python : le module standard qui simplifie vos itérations

Publié le par Gabriel Trouvé (mis à jour le )
75 minutes

Voici un guide que je voulais écrire depuis quelque temps. Itertools est un module soit méconnu, soit un peu nébuleux pour certains qui le découvrent. De plus, c'est une bibliothèque standard.

Après cette lecture, je parie que vous penserez à moi (ou du moins à ce module !) lors de vos prochains défis de code, au moment d'optimiser une boucle ou pour résoudre plus facilement certains algorithmes de l'examen TOSA Python 😛.

Itérable et itérateur

Avant de rentrer dans le vif du sujet, revenons sur les concepts d'itérable et d'itérateur.

En Python, tout objet sur lequel on peut boucler (for) est un itérable. Un itérable est capable de fournir un itérateur : l'objet qui permet de parcourir les éléments.
La gestion de la mémoire diffère selon le type d'itérable : 

  • Les conteneurs (par exemple les listes, tuples ou dictionnaires) sont des itérables qui stockent toutes leurs données en mémoire. Qu'ils contiennent un ou plusieurs éléments, toutes les valeurs sont présentes simultanément. Vous imaginez bien qu'avec un milliard d'éléments, la mémoire risque d'être mise à l'épreuve

  • Les itérateurs sont des objets qui fournissent les valeurs une par une, à la demande, lors du parcours. Contrairement aux conteneurs, ils ne stockent pas toutes les données en mémoire. Un itérateur est à la fois un itérable (on peut boucler dessus) et un objet qui sait produire le prochain élément via la méthode __next__()

Quand vous faites for element in ma_liste:, Python appelle iter(ma_liste) pour obtenir un itérateur.

À noter

Tous les itérateurs sont des itérables, mais l'inverse n'est pas vrai. Par exemple, une liste est un itérable qui peut créer plusieurs itérateurs pour être parcourue plusieurs fois, mais elle n'est pas elle-même un itérateur.

Pourquoi cette précision ? Parce qu'itertools fonctionne entièrement avec des itérateurs.

Les itérateurs infinis

Certaines fonctions d'itertools permettent de générer des valeurs à l'infini.

count : compter sans fin

La fonction count est parfaite pour indexer des données alors qu'on ne connaît pas la taille de notre structure d'avance ou pour générer des éléments avec un pas spécifique.

Syntaxe : count(start, step).

import itertools

joueurs = ["Mario", "Luigi", "Peach", "Toad"]

# On veut des IDs commençant à 100, par pas de 10 (100, 110, 120...)
# ...sans se soucier de la longueur de la liste 'joueurs'
ids_gen = itertools.count(start=100, step=10)

# zip va s'arrêter automatiquement dès que la liste 'joueurs' est finie
for uid, nom in zip(ids_gen, joueurs):
    print(f"ID {uid} : {nom}")

# Résultat :
# ID 100 : Mario
# ID 110 : Luigi
# ID 120 : Peach
# ID 130 : Toad
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

cycle : le remplaçant du modulo

La fonction cycle prend une séquence et la répète indéfiniment. Elle permet de remplacer l'opérateur modulo quand on veut boucler sur une liste.

couleurs = ['Rouge', 'Vert', 'Bleu']

for i in range(5):
    # On doit gérer l'index et la longueur
    # i % 3 donne : 0, 1, 2, 0, 1...
    print(couleurs[i % len(couleurs)])
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

À noter

L'opérateur % retourne le reste de la division entière. Par exemple, si on divise par 3 (la longueur de notre liste), les restes possibles sont toujours 0, 1 ou 2. Cela permet de revenir à 0 dès qu'on dépasse la fin de la liste, créant ainsi une boucle.

Donc, si je reprends l'exemple ci-dessus :

i = 0  → 0 % 3 = 0  → couleurs[0] = 'Rouge'
i = 1  → 1 % 3 = 1  → couleurs[1] = 'Vert'
i = 2  → 2 % 3 = 2  → couleurs[2] = 'Bleu'
i = 3  → 3 % 3 = 0  → couleurs[0] = 'Rouge'  ← On revient au début !
i = 4  → 4 % 3 = 1  → couleurs[1] = 'Vert'
i = 5  → 5 % 3 = 2  → couleurs[2] = 'Bleu'
i = 6  → 6 % 3 = 0  → couleurs[0] = 'Rouge'  ← Et on reboucle !
HTML

Dans un cas comme celui-ci, avec cycle, nous pouvons faire la même chose sans effectuer le calcul manuellement :

import itertools


couleurs = ['Rouge', 'Vert', 'Bleu']
cycle_couleurs = itertools.cycle(couleurs)

for _ in range(5):
    print(next(cycle_couleurs))
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Les itérateurs combinatoires

itertools offre la possibilité de se passer des boucles imbriquées et de gérer la complexité combinatoire.

product : se passer des boucles imbriquées

Prenons un exemple :

jeux = ['The Last of Us', 'Halo Infinite', 'Arc Raiders']
difficulte = ['Facile', 'Moyen', 'Difficile']

for j in jeux:
    for d in difficulte:
        print(j, d)

# The Last of Us Facile
# The Last of Us Moyen
# The Last of Us Difficile
# Halo Infinite Facile
# Halo Infinite Moyen
# Halo Infinite Difficile
# Arc Raiders Facile
# Arc Raiders Moyen
# Arc Raiders Difficile
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Simplifions le script avec product(*iterables, repeat=1) :

  • On peut passer autant d'itérables que l'on veut

  • repeat permet de répéter l'itérable avec lui-même. Il prend tout son sens lorsqu'on l'utilise avec un seul itérable pour le combiner avec lui-même

import itertools


# Exemple 1 : Simuler 2 lancers de dés (6 faces)
# Cela remplace product(range(1, 7), range(1, 7))
lancers = list(itertools.product(range(1, 7), repeat=2))
print(lancers)
# Sortie : [(1, 1), (1, 2), (1, 3), ..., (6, 5), (6, 6)]


# Exemple 2 : Simuler 3 lancers de dés (6 faces)
# Cela remplace product(range(1, 7), range(1, 7), range(1, 7))
lancers = list(itertools.product(range(1, 7), repeat=3))
print(lancers)
# Sortie : [(1, 1, 1), (1, 1, 2), (1, 1, 3), ..., (6, 6, 5), (6, 6, 6)]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Et si nous simplifiions notre exemple de boucles imbriquées ?

import itertools


jeux = ['The Last of Us', 'Halo Infinite', 'Arc Raiders']
difficulte = ['Facile', 'Moyen', 'Difficile']

for j, d in itertools.product(jeux, difficulte):
    print(j, d)

# The Last of Us Facile
# The Last of Us Moyen
# The Last of Us Difficile
# Halo Infinite Facile
# Halo Infinite Moyen
# Halo Infinite Difficile
# Arc Raiders Facile
# Arc Raiders Moyen
# Arc Raiders Difficile
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

permutations et combinations : générer tous les arrangements possibles

Ces deux fonctions se ressemblent, mais il faut noter une nuance concernant l'ordre :

  • permutations(iterable, r=None)

  • combinations(iterable, r)

Revenons sur les paramètres : 

  • iterable est la collection d'éléments à mélanger

  • r est la longueur des tuples générés. Pour permutations, si r n'est pas précisé, il vaut par défaut la longueur de l'itérable pour que tous les éléments soient arrangés. Cependant, pour combinations, r est obligatoire, car il faut spécifier combien d'éléments vous souhaitez récupérer

La différence se situe dans l'ordre des éléments : 

  • Pour permutations, l'ordre compte (AB sera différent de BA)

  • Pour combinations, l'ordre ne compte pas (AB est considéré comme identique à BA)

On pourrait imaginer générer toutes les combinaisons possibles pour un système de recommandation de produits, un système de recommandation de decks, etc.

import itertools


items = ['A', 'B', 'C']

# Permutations : L'ordre compte (AB est différent de BA)
print("Permutations :", list(itertools.permutations(items, 2)))
# Résultat : [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

# Combinaisons : L'ordre ne compte pas (AB est pareil que BA)
print("Combinaisons :", list(itertools.combinations(items, 2)))
# Résultat : [('A', 'B'), ('A', 'C'), ('B', 'C')]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Les itérateurs finis

Contrairement aux itérateurs infinis, ceux que nous allons voir s'arrêtent quand les données ont été traitées. Nous allons en sélectionner quelques-uns, mais vous pouvez retrouver la liste complète dans la documentation officielle.

Fusionner les données

La fonction chain permet de coller plusieurs itérateurs bout à bout pour en faire une seule séquence. Cela permet d'éviter de créer une nouvelle liste en mémoire.

  • chain(*iterables)
import itertools


list1 = [1, 2, 3]
list2 = [4, 5, 6]

for i in itertools.chain(list1, list2):
    print(i)
# Résultat : 1 2 3 4 5 6
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Vous connaissez la fonction zip ? Ce qui peut poser problème, c'est que dès que la liste la plus courte est épuisée, les données inutilisées de la liste la plus longue sont "perdues".

itertools fournit la fonction zip_longest. Souvent méconnue, elle permet d'aller au bout de la liste la plus longue. Les éléments manquants sont remplis avec une valeur par défaut que vous devez spécifier, sinon ce sera None.

  • zip_longest(*iterables, fillvalue=None)
import itertools


keys = ['Nom', 'Age', 'Ville']
values = ['Patrick', 25] # Il manque la ville 

result = list(itertools.zip_longest(keys, values, fillvalue='Inconnu'))

print(result)
# [('Nom', 'Patrick'), ('Age', 25), ('Ville', 'Inconnu')]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

La fonction accumulate permet de calculer par défaut des sommes cumulées. Cependant, il est possible de passer une fonction en argument pour personnaliser son comportement.

  • accumulate(iterable, func=operator.add, *, initial=None)
from itertools import accumulate
import operator

# Par défaut : sommes cumulées
print(list(accumulate([1, 2, 3, 4, 5])))
# → [1, 3, 6, 10, 15]

# Produits cumulés
print(list(accumulate([1, 2, 3, 4, 5], operator.mul)))
# → [1, 2, 6, 24, 120]

# Maximum cumulé
print(list(accumulate([5, 1, 8, 3, 9], max)))
# → [5, 5, 8, 8, 9]

# Fonction custom
print(list(accumulate([10, 5, 3, 2], lambda x, y: x - y)))
# → [10, 5, 2, 0]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

À noter

Au lieu de passer une fonction lambda pour changer le comportement, il est possible d'utiliser des fonctions standard comme operator.mul en passant par le module operator. Cela s'avère très pratique pour les opérations de base.

Découper

Commençons par la fonction islice

  • islice(iterable, start, stop, step=1)

Contrairement au slicing classique qui copie les données dans une nouvelle liste, islice permet de créer un itérateur sur la liste existante :

import itertools


ma_liste = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Création d'une nouvelle liste (Copie)
copie = ma_liste[2:5] 

# Création d'un itérateur basé sur une tranche de la liste originale
iterateur = itertools.islice(ma_liste, 2, 5)
print(list(iterateur)) # Affiche [2, 3, 4]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Aussi, avez-vous déjà essayé d'utiliser le slicing sur un générateur ? Python ne le permet pas, mais on peut contourner ce problème :

import itertools


generateur = (x for x in range(10))

tranche = itertools.islice(generateur, 3, 7)
print(list(tranche))  # Affiche [3, 4, 5, 6]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

batched, la petite nouvelle arrivée avec Python 3.12 et modifiée depuis Python 3.13, permet de découper un itérable en tuples de taille n

  • batched(iterable, n, strict=False)

Si le paramètre strict (ajouté dans la version 3.13) est à True, Python lèvera une ValueError si le dernier lot est incomplet (donc plus court que n).

import itertools


compagnie = ['Frodon', 'Sam', 'Gandalf', 'Aragorn', 'Legolas', 'Gimli', 'Boromir']


# Le dernier lot ('Boromir',) est plus court, mais strict=False par défaut
lots = list(itertools.batched(compagnie, 3))
print(lots)
# Résultat : [('Frodo', 'Sam', 'Gandalf'), ('Aragorn', 'Legolas', 'Gimli'), ('Boromir',)]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

En réglant le paramètre strict sur True :

import itertools


compagnie = ['Frodon', 'Sam', 'Gandalf', 'Aragorn', 'Legolas', 'Gimli', 'Boromir']


# Le dernier lot ('Boromir',) est plus court, donc une exception est levée
lots = list(itertools.batched(compagnie, 3, strict=True))
print(lots)
# Résultat : ValueError: batched(): incomplete batch
PYTHON

Ajoutée dans Python 3.10, pairwise renvoie des paires glissantes : 

  • pairwise(iterable)
import itertools


cartes = ['Lightning Bolt', 'Counterspell', 'Black Lotus', 'Mox Sapphire', 'Ancestral Recall', 'Brainstorm']

# Paires successives
paires = list(itertools.pairwise(cartes))
print(paires)
# Résultat : [('Lightning Bolt', 'Counterspell'), ('Counterspell', 'Black Lotus'), ('Black Lotus', 'Mox Sapphire'), ('Mox Sapphire', 'Ancestral Recall'), ('Ancestral Recall', 'Brainstorm')]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Filtrer et grouper

Vous avez peut-être déjà rencontré la fonction filter ? C'est une fonction native peu populaire, car on préfère généralement utiliser les compréhensions de liste. Cependant, là où une compréhension de liste génère une liste, la fonction filter renvoie un itérateur :

# Récupérer les nombres pairs

# Avec filter()
nombres = [1, 2, 3, 4, 5, 6]
pairs = filter(lambda x: x % 2 == 0, nombres)
print(pairs) # <filter object at 0x...>

# Équivalent en compréhension de liste
pairs = [x for x in nombres if x % 2 == 0]
print(pairs) # [2, 4, 6]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Vous me voyez venir ? Tout cela pour dire qu'itertools propose la fonction filterfalse

  • filterfalse(predicate, iterable)

À noter

predicate correspond à une fonction qui renvoie True ou False.

filterfalse est donc l'inverse de la fonction filter : elle garde les éléments qui ne satisfont pas la condition.

Tout comme pour filter, on peut utiliser une compréhension de liste, mais filterfalse renvoie un itérateur :

import itertools


nombres = [1, 2, 3, 4, 5, 6]
impairs = itertools.filterfalse(lambda x: x % 2 == 0, nombres)
print(impairs) # <itertools.filterfalse object at 0x...>
print(list(impairs)) # [1, 3, 5]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Toute bonne chose a une fin. Nous avons vu quelques fonctions intéressantes proposées par itertools, mais nous allons clôturer cet article avec groupby.

La fonction groupby permet de regrouper les données, à condition que celles-ci soient préalablement triées. Autrement dit, cette fonction regroupe les éléments consécutifs identiques.

  • groupby(iterable, key=None)

iterable correspond à la séquence que l'on veut grouper, tandis que key (optionnel) attend une fonction de regroupement.

Prenons un exemple sans tri préalable :

from itertools import groupby


nombres = [1, 2, 1, 2, 1, 3, 3]

for cle, groupe in groupby(nombres):
    print(f"{cle}: {list(groupe)}")

# Résultat sans tri:
# 1: [1]
# 2: [2]
# 1: [1]
# 2: [2]
# 1: [1]
# 3: [3, 3]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Attention

groupby crée un nouveau groupe dès qu'elle rencontre une valeur différente.

Avec la liste triée :

from itertools import groupby


nombres = [1, 2, 1, 2, 1, 3, 3]
nombres.sort()


for cle, groupe in groupby(nombres):
    print(f"{cle}: {list(groupe)}")

# Résultat avec tri:
# 1: [1, 1, 1]
# 2: [2, 2]
# 3: [3, 3]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Voici un autre exemple utilisant une liste de dictionnaires :

from itertools import groupby


# Exemple avec des appareils domotiques Shelly
appareils = [
    {'nom': 'Shelly Plus 1', 'type': 'Switch', 'piece': 'Salon'},
    {'nom': 'Shelly Plug S', 'type': 'Plug', 'piece': 'Cuisine'},
    {'nom': 'Shelly Dimmer 2', 'type': 'Dimmer', 'piece': 'Salon'},
    {'nom': 'Shelly Plus 2PM', 'type': 'Switch', 'piece': 'Buanderie'},
    {'nom': 'Shelly RGBW2', 'type': 'Light', 'piece': 'Salon'}
]

# Trier selon le type
appareils_tries = sorted(appareils, key=lambda x: x['type'])

# Grouper
for cle, groupe in groupby(appareils_tries, key=lambda x: x['type']):
    print(f"{cle}: {list(groupe)}")

# Résultat:
# Dimmer: [{'nom': 'Shelly Dimmer 2', 'type': 'Dimmer', 'piece': 'Salon'}]
# Light: [{'nom': 'Shelly RGBW2', 'type': 'Light', 'piece': 'Salon'}]
# Plug: [{'nom': 'Shelly Plug S', 'type': 'Plug', 'piece': 'Cuisine'}]
# Switch: [{'nom': 'Shelly Plus 1', 'type': 'Switch', 'piece': 'Salon'}, {'nom': 'Shelly Plus 2PM', 'type': 'Switch', 'piece': 'Buanderie'}]
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Terminons par un exemple montrant comment grouper des éléments par leur première lettre, ce qui illustre parfaitement l'utilité de l'argument key :

from itertools import groupby

mots = ['arbre', 'avion', 'banane', 'ballon', 'chat', 'chien']

# Déjà trié alphabétiquement, vous avez compris l'idée 😛
for lettre, groupe in groupby(mots, key=lambda mot: mot[0]):
    print(f"Lettre {lettre}: {list(groupe)}")

# Résultat :
# Lettre a: ['arbre', 'avion']
# Lettre b: ['banane', 'ballon']
# Lettre c: ['chat', 'chien']
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Pour aller plus loin, je vous invite à consulter la documentation officielle ainsi que la bibliothèque tierce more-itertools.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.