La librairie operator fait partie de la bibliothèque standard de Python depuis toujours, et pourtant beaucoup de développeurs continuent d’écrire lambda x: x[0] ou lambda obj: obj.nom quand une fonction de operator ferait le même travail, en plus rapide et plus lisible. Comprendre ce que cette librairie offre, et comment elle est implémentée, change la façon dont on écrit du code fonctionnel en Python.
Ce que contient operator
operator expose des fonctions qui correspondent aux opérateurs du langage. operator.add(2, 3) est l’équivalent fonctionnel de 2 + 3, operator.lt(a, b) correspond à a < b. L’intérêt n’est pas de remplacer les opérateurs dans du code arithmétique ordinaire, ce serait absurde. L’intérêt est de pouvoir passer une opération comme argument à une fonction d’ordre supérieur (map, filter, sorted, reduce, functools.partial).
import operator
from functools import reduce
# Somme d'une liste, sans lambda
total = reduce(operator.add, [1, 2, 3, 4]) # 10
# Produit cumulatif
produit = reduce(operator.mul, [1, 2, 3, 4]) # 24
operator.add est une référence à une fonction compilée en C dans CPython. La passer à reduce évite la création d’une frame Python à chaque appel, ce qui se mesure sur de gros volumes.
Une frame, c’est quoi ? À chaque appel de fonction Python, l’interpréteur alloue un objet
framequi contient les variables locales, la pile d’évaluation, la position courante dans le bytecode et une référence à la frame appelante. C’est ce qui apparaît dans une stack trace. Allouer une frame n’est pas gratuit : il faut créer l’objet, initialiser ses champs, le détruire au retour. Une fonction écrite en C (commeoperator.addouitemgetter) n’a pas besoin de frame Python, elle s’exécute directement dans la VM. C’est cette économie, multipliée par des millions d’itérations, qui rendoperatormesurable.
itemgetter : l’accès par clé ou index
operator.itemgetter(key) retourne un callable qui, appliqué à un objet, retourne obj[key]. C’est l’équivalent fonctionnel de lambda obj: obj[key], en plus rapide.
from operator import itemgetter
data = [(1, "a"), (3, "c"), (2, "b")]
# Avec lambda
sorted(data, key=lambda t: t[0])
# Avec itemgetter
sorted(data, key=itemgetter(0))
Sur ce petit jeu de données la différence est invisible. Sur une liste de 100 000 tuples, itemgetter est typiquement 20 à 40 % plus rapide qu’une lambda équivalente, parce que l’extraction est faite en C sans repasser par l’interpréteur Python à chaque appel.
itemgetter accepte plusieurs clés et retourne alors un tuple :
from operator import itemgetter
utilisateurs = [
{"nom": "Alice", "age": 30, "ville": "Paris"},
{"nom": "Bob", "age": 25, "ville": "Lyon"},
{"nom": "Alice", "age": 28, "ville": "Lyon"},
]
# Tri multi-critères : par nom puis par age
tries = sorted(utilisateurs, key=itemgetter("nom", "age"))
L’équivalent en lambda serait lambda u: (u["nom"], u["age"]). Lisible, mais plus verbeux et plus lent.
attrgetter : l’accès aux attributs
attrgetter fait pour les attributs ce que itemgetter fait pour les clés. Il accepte aussi les attributs imbriqués via la notation pointée.
from operator import attrgetter
from dataclasses import dataclass
@dataclass
class Adresse:
ville: str
code_postal: str
@dataclass
class Utilisateur:
nom: str
adresse: Adresse
users = [
Utilisateur("Alice", Adresse("Paris", "75001")),
Utilisateur("Bob", Adresse("Lyon", "69001")),
]
# Tri par ville (attribut imbriqué)
sorted(users, key=attrgetter("adresse.ville"))
# Extraction parallèle de plusieurs attributs
get_id_ville = attrgetter("nom", "adresse.ville")
get_id_ville(users[0]) # ('Alice', 'Paris')
L’avantage par rapport à lambda u: u.adresse.ville n’est pas que théorique. Sur une grosse collection, l’accès en C aux attributs économise des allocations de frames Python. Et la signature est auto-documentée : attrgetter("adresse.ville") dit exactement ce qu’il extrait.
methodcaller : appeler une méthode avec des arguments fixes
methodcaller(nom, *args, **kwargs) retourne un callable qui appelle la méthode nom sur son argument, en lui passant args et kwargs.
from operator import methodcaller
phrases = ["bonjour", "monde", "python"]
en_majuscules = list(map(methodcaller("upper"), phrases))
# ['BONJOUR', 'MONDE', 'PYTHON']
# Avec arguments
csv = ["a,b,c", "d,e,f"]
splits = list(map(methodcaller("split", ","), csv))
# [['a', 'b', 'c'], ['d', 'e', 'f']]
C’est particulièrement utile quand on construit une pipeline de transformations sans vouloir écrire une lambda par étape. La version équivalente lambda s: s.split(",") fonctionne, mais methodcaller("split", ",") est légèrement plus rapide et indique clairement l’intention.
Les opérateurs arithmétiques et de comparaison
operator expose tous les opérateurs du langage sous forme de fonctions :
| Opération | Fonction |
|---|---|
a + b | operator.add(a, b) |
a - b | operator.sub(a, b) |
a * b | operator.mul(a, b) |
a / b | operator.truediv(a, b) |
a // b | operator.floordiv(a, b) |
a % b | operator.mod(a, b) |
a ** b | operator.pow(a, b) |
a < b | operator.lt(a, b) |
a <= b | operator.le(a, b) |
a == b | operator.eq(a, b) |
a > b | operator.gt(a, b) |
a is b | operator.is_(a, b) |
not a | operator.not_(a) |
Tous existent aussi en version “in-place” pour les opérateurs augmentés (iadd, imul, etc.), qui correspondent à a += b. Sur les types immuables comme int ou tuple, iadd se comporte comme add parce que ces types ne peuvent pas être modifiés sur place. Sur une liste, iadd mute l’objet et le retourne, ce qui est le comportement standard de += en Python. Pour les détails du mécanisme, voir Python add et iadd : la différence qui change tout.
Cas d’usage avancés
Tri stable multi-passes. Pour trier par plusieurs critères dans des sens différents (croissant puis décroissant), sorted est stable, donc on enchaîne les tris du critère le moins important au plus important :
from operator import itemgetter
# Tri par age décroissant, puis par nom croissant (en cas d'égalité d'age)
data = [
{"nom": "Alice", "age": 30},
{"nom": "Bob", "age": 30},
{"nom": "Charlie", "age": 25},
]
# Première passe : par nom (critère secondaire)
data = sorted(data, key=itemgetter("nom"))
# Deuxième passe : par age décroissant (critère principal)
data = sorted(data, key=itemgetter("age"), reverse=True)
Group-by avec itertools.groupby. groupby requiert un callable de clé, et itemgetter est l’outil naturel :
from itertools import groupby
from operator import itemgetter
ventes = [
{"region": "EU", "montant": 100},
{"region": "EU", "montant": 200},
{"region": "US", "montant": 150},
]
# groupby exige que la donnée soit triée par la clé de groupement
ventes_triees = sorted(ventes, key=itemgetter("region"))
for region, items in groupby(ventes_triees, key=itemgetter("region")):
total = sum(v["montant"] for v in items)
print(region, total)
Réduction avec opérateurs. Toutes les agrégations classiques peuvent se réduire avec reduce et un opérateur :
from functools import reduce
from operator import add, mul, and_, or_
# Concaténation de listes (à éviter sur de gros volumes)
reduce(add, [[1, 2], [3, 4], [5, 6]]) # [1, 2, 3, 4, 5, 6]
# Intersection de sets
reduce(and_, [{1, 2, 3}, {2, 3, 4}, {3, 4, 5}]) # {3}
# Union de sets
reduce(or_, [{1, 2}, {2, 3}, {3, 4}]) # {1, 2, 3, 4}
Pour la somme numérique, sum() reste plus idiomatique et plus rapide que reduce(add, ...). Pour le produit, math.prod() existe depuis Python 3.8.
operator vs lambda : la mesure
Le gain de performance vient de l’implémentation en C. Un petit benchmark sur un tri de 100 000 dictionnaires :
import timeit
from operator import itemgetter
data = [{"k": i, "v": i * 2} for i in range(100_000)]
t_lambda = timeit.timeit(
lambda: sorted(data, key=lambda d: d["k"]),
number=100,
)
t_getter = timeit.timeit(
lambda: sorted(data, key=itemgetter("k")),
number=100,
)
print(f"lambda: {t_lambda:.2f}s")
print(f"itemgetter: {t_getter:.2f}s")
Sur CPython 3.12, on observe en général un ratio de 1,2x à 1,5x en faveur d’itemgetter selon la machine. Ce n’est pas spectaculaire, mais c’est gratuit, et le code est plus court. Sur des opérations critiques en boucle serrée (map, filter sur des millions d’éléments), le gain devient visible.
L’autre dimension est la picklabilité. itemgetter("k") est sérialisable par pickle, alors qu’une lambda ne l’est pas. C’est important quand on passe une fonction de clé à un pool de processus via multiprocessing :
from multiprocessing import Pool
from operator import itemgetter
with Pool(4) as pool:
# Fonctionne
result = pool.map(itemgetter("k"), data)
# Échoue : PicklingError sur la lambda
# result = pool.map(lambda d: d["k"], data)
Quand ne pas utiliser operator
operator n’est pas une solution miracle. Quand la logique d’extraction est non triviale (calcul, condition, accès conditionnel), une fonction nommée reste plus lisible :
# Mauvais usage : illisible
sorted(data, key=lambda x: itemgetter("score")(x) if x["actif"] else 0)
# Mieux : fonction nommée
def cle_tri(x):
return x["score"] if x["actif"] else 0
sorted(data, key=cle_tri)
operator brille sur les cas simples et répétitifs. Au-delà, une fonction dédiée garde le code clair.
Ce qu’il faut retenir
La librairie operator ne change pas radicalement la performance d’un programme, mais elle élimine une catégorie entière de lambdas triviales qui polluent le code. itemgetter, attrgetter et methodcaller sont les trois outils à intégrer en premier dans son réflexe quotidien : ils rendent les clés de tri et les pipelines fonctionnels auto-documentés, légèrement plus rapides, et picklables. Les opérateurs arithmétiques et de comparaison complètent l’éventail pour les cas de réduction. Comprendre operator, c’est apprendre à exprimer des opérations comme des valeurs de première classe, ce qui est l’une des forces sous-utilisées de Python.
