Aller au contenu
Développement medium

Python itertools et functools : le fonctionnel utile

16 min de lecture

logo python

itertools et functools sont deux modules de la bibliothèque standard qui outillent la programmation fonctionnelle en Python : composer des itérateurs paresseux et transformer des fonctions. Le premier fournit des briques pour chainer, grouper, combiner ou accumuler des séquences sans écrire de boucle verbeuse. Le second apporte la mémoïsation (lru_cache), l'application partielle (partial), la surcharge par type (singledispatch) et de quoi écrire des décorateurs propres (wraps). Ce guide montre quand chaque outil rend le code plus lisible et plus rapide, avec des exemples que vous pouvez rejouer.

Il s'adresse aux développeurs qui connaissent déjà les fonctions, les générateurs et les décorateurs, et qui veulent remplacer des boucles maison par des outils testés et optimisés en C. Les exemples ont été exécutés avec Python 3.12.

  • Chainer et découper des itérateurs avec chain et islice.
  • Grouper, combiner et accumuler avec groupby, product et accumulate.
  • Réduire une séquence à une valeur avec reduce.
  • Figer des arguments avec partial et mémoïser avec lru_cache.
  • Écrire des décorateurs propres avec wraps et surcharger par type avec singledispatch.

Le module itertools renvoie des itérateurs paresseux : ils produisent leurs valeurs une par une, à la demande, sans construire la liste complète en mémoire. C'est le même principe que les générateurs, appliqué à des opérations courantes. Sur un flux de logs ou un gros CSV, cette paresse évite de saturer la RAM. Pour inspecter le résultat dans les exemples, on le convertit en list(), mais en production on itère directement dessus.

Ces trois fonctions produisent des itérateurs infinis ou répétitifs. count compte à partir d'une valeur avec un pas, cycle répète une séquence en boucle, repeat renvoie toujours la même valeur. Comme count et cycle ne s'arrêtent jamais, on les borne toujours avec islice ou une condition, sinon la boucle tourne à l'infini.

from itertools import count, cycle, repeat, islice
compteur = count(start=10, step=5)
print(list(islice(compteur, 4))) # borne à 4 valeurs
couleurs = cycle(["rouge", "vert", "bleu"])
print([next(couleurs) for _ in range(5)])
print(list(repeat("ok", 3)))
[10, 15, 20, 25]
['rouge', 'vert', 'bleu', 'rouge', 'vert']
['ok', 'ok', 'ok']

count sert par exemple à numéroter des éléments, cycle à distribuer un travail entre serveurs à tour de rôle (round-robin), et repeat à fournir une valeur constante à map ou zip.

chain enchaîne plusieurs itérables comme s'ils n'en formaient qu'un, sans créer de liste intermédiaire. islice découpe un itérateur à la manière du slicing de liste, mais il fonctionne sur n'importe quel itérable, y compris infini ou non indexable. C'est le bon réflexe pour parcourir les 100 premières lignes d'un fichier sans tout charger.

from itertools import chain, islice
frontend = ["react", "vue"]
backend = ["django", "fastapi"]
print(list(chain(frontend, backend)))
# les 4 premiers carrés, sans matérialiser la liste infinie
carres = (n * n for n in count(1))
print(list(islice(carres, 4)))
['react', 'vue', 'django', 'fastapi']
[1, 4, 9, 16]

groupby regroupe les éléments consécutifs qui partagent une même clé. Le piège classique : il ne groupe que les éléments adjacents, donc les données doivent être triées sur la clé au préalable, exactement comme la commande Unix sort | uniq. Sans tri, un même groupe apparaîtra plusieurs fois.

from itertools import groupby
serveurs = [
{"role": "web", "nom": "web-01"},
{"role": "web", "nom": "web-02"},
{"role": "db", "nom": "db-01"},
{"role": "cache", "nom": "cache-01"},
]
serveurs.sort(key=lambda s: s["role"]) # indispensable avant groupby
for role, groupe in groupby(serveurs, key=lambda s: s["role"]):
noms = [s["nom"] for s in groupe]
print(role, noms)
cache ['cache-01']
db ['db-01']
web ['web-01', 'web-02']

Notez que groupe est lui-même un itérateur paresseux : on le consomme dans la foulée avec une compréhension. Si vous devez le réutiliser, convertissez-le en liste immédiatement, car il se vide dès la première itération.

Combiner avec product, permutations et combinations

Section intitulée « Combiner avec product, permutations et combinations »

Ces trois fonctions génèrent des combinaisons sans boucles imbriquées. product calcule le produit cartésien (toutes les paires possibles entre plusieurs ensembles), permutations produit les arrangements ordonnés, et combinations les groupes sans ordre ni répétition. C'est idéal pour construire une matrice de tests ou explorer un espace de paramètres.

from itertools import product, permutations, combinations
environnements = ["dev", "prod"]
regions = ["eu-west-2", "cloudgouv-eu-west-1"]
for env, region in product(environnements, regions):
print(env, region)
print(list(permutations([1, 2, 3], 2)))
print(list(combinations(["alice", "bob", "carol"], 2)))
dev eu-west-2
dev cloudgouv-eu-west-1
prod eu-west-2
prod cloudgouv-eu-west-1
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
[('alice', 'bob'), ('alice', 'carol'), ('bob', 'carol')]

product remplace avantageusement deux for imbriqués. La distinction utile : permutations tient compte de l'ordre ((1, 2) diffère de (2, 1)), alors que combinations ignore l'ordre et ne retient chaque groupe qu'une fois.

Cumuler avec accumulate et aligner avec zip_longest

Section intitulée « Cumuler avec accumulate et aligner avec zip_longest »

accumulate produit des totaux courants (somme cumulée par défaut, ou toute autre opération binaire). zip_longest assemble plusieurs séquences de longueurs différentes en comblant les trous avec une valeur de remplissage, là où le zip natif s'arrête au plus court et perd les éléments en trop.

from itertools import accumulate, zip_longest
ventes = [100, 250, 75, 300]
print(list(accumulate(ventes))) # somme cumulée
print(list(accumulate(ventes, max))) # maximum courant
noms = ["Alice", "Bob", "Carol"]
scores = [15, 12]
print(list(zip_longest(noms, scores, fillvalue=0)))
[100, 350, 425, 725]
[100, 250, 250, 300]
[('Alice', 15), ('Bob', 12), ('Carol', 0)]

accumulate sert à tracer une courbe cumulée de chiffre d'affaires ou un maximum glissant. zip_longest évite le bug silencieux du zip classique quand vos deux listes n'ont pas exactement la même taille.

functools : transformer et optimiser des fonctions

Section intitulée « functools : transformer et optimiser des fonctions »

Là où itertools agit sur des données, functools agit sur les fonctions elles-mêmes. Il permet de les spécialiser, de mettre en cache leurs résultats, de préserver leurs métadonnées quand on les décore et de les surcharger selon le type reçu. Ces outils rendent le code plus déclaratif et suppriment beaucoup de code répétitif.

reduce replie une séquence en une seule valeur en appliquant une fonction à deux arguments, de proche en proche. Beaucoup de cas se traitent plus lisiblement avec sum, max ou une compréhension : réservez reduce aux agrégations personnalisées qui n'ont pas d'équivalent natif, comme un produit ou une fusion de dictionnaires.

from functools import reduce
nombres = [2, 3, 4, 5]
produit = reduce(lambda acc, x: acc * x, nombres)
print(produit) # 2 * 3 * 4 * 5
120

L'accumulateur acc transporte le résultat partiel à chaque étape. Si la lisibilité souffre, une simple boucle for reste souvent préférable : reduce brille surtout quand l'opération d'agrégation est claire et associative.

partial crée une nouvelle fonction en pré-remplissant certains arguments d'une fonction existante. On évite ainsi de réécrire des lambdas ou de répéter les mêmes paramètres. C'est très pratique pour spécialiser une fonction de configuration ou fournir un callback déjà paramétré.

from functools import partial
def connexion(hote, port, timeout):
return f"{hote}:{port} (timeout={timeout}s)"
connexion_locale = partial(connexion, "localhost", timeout=5)
print(connexion_locale(5432))
print(connexion_locale(6379))
localhost:5432 (timeout=5s)
localhost:6379 (timeout=5s)

La fonction connexion_locale a figé l'hôte et le timeout ; il ne reste qu'à passer le port. Le code appelant devient plus court et exprime mieux son intention que de répéter "localhost" partout.

lru_cache mémorise les résultats d'une fonction en fonction de ses arguments : au second appel avec les mêmes entrées, le résultat est renvoyé instantanément sans recalcul. Cette mémoïsation transforme des algorithmes récursifs exponentiels en calculs quasi immédiats. Depuis Python 3.9, cache est un raccourci équivalent à lru_cache(maxsize=None).

from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(30))
print(fibonacci.cache_info())
832040
CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)

Le gain est spectaculaire. Sur un calcul de fibonacci(32) mesuré en local, la version sans cache prend 147 ms contre 0,02 ms avec lru_cache, soit un facteur de plusieurs milliers. La méthode cache_info() donne le nombre de hits (résultats servis depuis le cache) et de misses (vrais calculs), utile pour vérifier l'efficacité.

Quand on écrit un décorateur, la fonction interne remplace l'originale et masque son nom et sa docstring. wraps recopie ces métadonnées sur l'enveloppe, ce qui garde un code débogable et une documentation intacte. C'est un réflexe obligatoire sur tout décorateur sérieux, comme le rappelle le guide sur les décorateurs.

from functools import wraps
def chronometre(fonction):
@wraps(fonction)
def enveloppe(*args, **kwargs):
return fonction(*args, **kwargs)
return enveloppe
@chronometre
def calculer(x):
"Calcule le carré de x."
return x * x
print(calculer.__name__)
print(calculer.__doc__)
calculer
Calcule le carré de x.

Sans @wraps, calculer.__name__ afficherait enveloppe et la docstring serait None. Ce détail casse les outils qui s'appuient sur l'introspection (générateurs de doc, débogueurs, frameworks de test).

Calculer une propriété une seule fois avec cached_property

Section intitulée « Calculer une propriété une seule fois avec cached_property »

cached_property transforme une méthode en attribut calculé à la demande, puis mémorisé sur l'instance. Le premier accès déclenche le calcul, les suivants renvoient la valeur stockée sans recalcul. C'est parfait pour une valeur coûteuse mais stable sur la durée de vie de l'objet, comme un total agrégé ou un résultat de requête.

from functools import cached_property
class Rapport:
def __init__(self, lignes):
self.lignes = lignes
@cached_property
def total(self):
print(" (calcul du total...)")
return sum(self.lignes)
r = Rapport([10, 20, 30])
print(r.total) # calcule
print(r.total) # sert le cache
(calcul du total...)
60
60

Le message de calcul n'apparaît qu'une fois : le second accès lit la valeur mémorisée. Contrairement à lru_cache, le cache vit sur l'instance et disparaît avec elle, ce qui évite les fuites mémoire à l'échelle du processus.

singledispatch implémente la surcharge de fonction selon le type du premier argument. Plutôt qu'une longue cascade de if isinstance(...), on enregistre une implémentation par type. Le code reste ouvert à l'extension : ajouter un nouveau type se fait en enregistrant une variante, sans toucher aux autres.

from functools import singledispatch
@singledispatch
def formater(valeur):
return f"inconnu: {valeur}"
@formater.register
def _(valeur: int):
return f"entier: {valeur:,}"
@formater.register
def _(valeur: list):
return f"liste de {len(valeur)} éléments"
@formater.register
def _(valeur: str):
return f"texte: {valeur.upper()}"
print(formater(1500000))
print(formater(["a", "b", "c"]))
print(formater("bonjour"))
print(formater(3.14)) # aucun type enregistré
entier: 1,500,000
liste de 3 éléments
texte: BONJOUR
inconnu: 3.14

La fonction décorée par @singledispatch sert de cas par défaut, appelé quand aucun type enregistré ne correspond (ici le float). Chaque variante se déclare avec l'annotation de type sur son paramètre, ce qui rejoint la logique des type hints.

Ces deux modules ne sont pas des gadgets : ils remplacent du code maison par des briques testées et optimisées en C. Utilisez itertools dès qu'une boucle manipule des séquences de façon standard (chaîner, grouper, combiner) ou quand la mémoire compte, grâce à l'évaluation paresseuse. Utilisez functools pour accélérer (mémoïsation), spécialiser (partial, singledispatch) ou fiabiliser vos décorateurs (wraps).

Le bon critère reste la lisibilité. Si reduce ou une combinaison alambiquée d'itertools devient plus obscure qu'une boucle explicite, gardez la boucle. Ces outils gagnent quand ils expriment une intention claire en moins de lignes, pas quand ils compressent le code au prix de la compréhension.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • itertools compose des itérateurs paresseux : peu de mémoire, pas de liste intermédiaire.
  • chain enchaîne des itérables, islice les découpe même quand ils sont infinis.
  • groupby regroupe des éléments adjacents : trier sur la clé avant est obligatoire.
  • product, permutations et combinations remplacent les boucles imbriquées.
  • accumulate produit des totaux courants ; zip_longest aligne des séquences de tailles différentes.
  • reduce replie une séquence en une valeur, à réserver aux agrégations sans équivalent natif.
  • partial fige des arguments ; lru_cache et cache mémoïsent les fonctions pures.
  • wraps préserve nom et docstring d'une fonction décorée ; cached_property calcule une fois par instance ; singledispatch surcharge par type.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn