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
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)])
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 !
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))
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
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
-
repeatpermet 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)]
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
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 :
-
iterableest la collection d'éléments à mélanger -
rest la longueur des tuples générés. Pourpermutations, sirn'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, pourcombinations,rest 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 (ABsera différent deBA) -
Pour
combinations, l'ordre ne compte pas (ABest 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')]
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
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')]
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]
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]
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]
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',)]
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
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')]
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]
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]
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]
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]
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'}]
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']
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.