La librería operator forma parte de la biblioteca estándar de Python desde siempre, y aun así muchos desarrolladores siguen escribiendo lambda x: x[0] o lambda obj: obj.nombre cuando una función de operator haría el mismo trabajo, de forma más rápida y más legible. Entender qué ofrece esta librería, y cómo está implementada, cambia la manera de escribir código funcional en Python.
Qué contiene operator
operator expone funciones que corresponden a los operadores del lenguaje. operator.add(2, 3) es el equivalente funcional de 2 + 3, operator.lt(a, b) corresponde a a < b. El interés no es reemplazar los operadores en código aritmético ordinario, sería absurdo. El interés es poder pasar una operación como argumento a una función de orden superior (map, filter, sorted, reduce, functools.partial).
import operator
from functools import reduce
# Suma de una lista, sin lambda
total = reduce(operator.add, [1, 2, 3, 4]) # 10
# Producto acumulativo
producto = reduce(operator.mul, [1, 2, 3, 4]) # 24
operator.add es una referencia a una función compilada en C en CPython. Pasarla a reduce evita la creación de un frame Python en cada llamada, lo que se mide en grandes volúmenes.
¿Qué es un frame? En cada llamada a una función Python, el intérprete asigna un objeto
frameque contiene las variables locales, la pila de evaluación, la posición actual en el bytecode y una referencia al frame llamante. Es lo que aparece en una traza de pila. Asignar un frame no es gratis: hay que crear el objeto, inicializar sus campos y destruirlo al retornar. Una función escrita en C (comooperator.addoitemgetter) no necesita un frame Python, se ejecuta directamente dentro de la VM. Ese ahorro, multiplicado por millones de iteraciones, es lo que vuelve aoperatormensurable.
itemgetter: el acceso por clave o índice
operator.itemgetter(key) devuelve un callable que, aplicado a un objeto, retorna obj[key]. Es el equivalente funcional de lambda obj: obj[key], pero más rápido.
from operator import itemgetter
data = [(1, "a"), (3, "c"), (2, "b")]
# Con lambda
sorted(data, key=lambda t: t[0])
# Con itemgetter
sorted(data, key=itemgetter(0))
En este pequeño conjunto la diferencia es invisible. Sobre una lista de 100 000 tuplas, itemgetter suele ser entre un 20 y un 40 % más rápido que la lambda equivalente, porque la extracción ocurre en C sin volver a pasar por el intérprete de Python en cada llamada.
itemgetter acepta varias claves y entonces retorna una tupla:
from operator import itemgetter
usuarios = [
{"nombre": "Alice", "edad": 30, "ciudad": "Madrid"},
{"nombre": "Bob", "edad": 25, "ciudad": "Barcelona"},
{"nombre": "Alice", "edad": 28, "ciudad": "Barcelona"},
]
# Orden multi-criterio: por nombre y luego por edad
ordenados = sorted(usuarios, key=itemgetter("nombre", "edad"))
El equivalente con lambda sería lambda u: (u["nombre"], u["edad"]). Legible, pero más extenso y más lento.
attrgetter: el acceso a los atributos
attrgetter hace por los atributos lo que itemgetter hace por las claves. Acepta además atributos anidados con notación de punto.
from operator import attrgetter
from dataclasses import dataclass
@dataclass
class Direccion:
ciudad: str
codigo_postal: str
@dataclass
class Usuario:
nombre: str
direccion: Direccion
usuarios = [
Usuario("Alice", Direccion("Madrid", "28001")),
Usuario("Bob", Direccion("Barcelona", "08001")),
]
# Orden por ciudad (atributo anidado)
sorted(usuarios, key=attrgetter("direccion.ciudad"))
# Extracción paralela de varios atributos
get_id_ciudad = attrgetter("nombre", "direccion.ciudad")
get_id_ciudad(usuarios[0]) # ('Alice', 'Madrid')
La ventaja frente a lambda u: u.direccion.ciudad no es solo teórica. Sobre una colección grande, el acceso en C a los atributos ahorra asignaciones de frames Python. Y la firma se documenta sola: attrgetter("direccion.ciudad") dice exactamente qué extrae.
methodcaller: invocar un método con argumentos fijos
methodcaller(nombre, *args, **kwargs) retorna un callable que llama al método nombre sobre su argumento, pasándole args y kwargs.
from operator import methodcaller
frases = ["hola", "mundo", "python"]
en_mayusculas = list(map(methodcaller("upper"), frases))
# ['HOLA', 'MUNDO', 'PYTHON']
# Con argumentos
csv = ["a,b,c", "d,e,f"]
splits = list(map(methodcaller("split", ","), csv))
# [['a', 'b', 'c'], ['d', 'e', 'f']]
Resulta especialmente útil al construir una pipeline de transformaciones sin tener que escribir una lambda por cada etapa. La versión equivalente lambda s: s.split(",") funciona, pero methodcaller("split", ",") es ligeramente más rápida y muestra la intención con claridad.
Operadores aritméticos y de comparación
operator expone todos los operadores del lenguaje como funciones:
| Operación | Función |
|---|---|
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) |
Todos existen también en versión “in-place” para los operadores aumentados (iadd, imul, etc.), que corresponden a a += b. Sobre tipos inmutables como int o tuple, iadd se comporta como add porque esos tipos no pueden modificarse in situ. Sobre una lista, iadd muta el objeto y lo retorna, que es el comportamiento estándar de += en Python. Para los detalles del mecanismo, ver Python add y iadd: la diferencia que lo cambia todo.
Casos de uso avanzados
Ordenación estable multi-pasada. Para ordenar por varios criterios en sentidos distintos (creciente y luego decreciente), sorted es estable, así que se encadenan los ordenamientos del criterio menos importante al más importante:
from operator import itemgetter
# Orden por edad decreciente, luego por nombre creciente (en caso de empate)
data = [
{"nombre": "Alice", "edad": 30},
{"nombre": "Bob", "edad": 30},
{"nombre": "Charlie", "edad": 25},
]
# Primera pasada: por nombre (criterio secundario)
data = sorted(data, key=itemgetter("nombre"))
# Segunda pasada: por edad decreciente (criterio principal)
data = sorted(data, key=itemgetter("edad"), reverse=True)
Group-by con itertools.groupby. groupby requiere un callable de clave, y itemgetter es la herramienta natural:
from itertools import groupby
from operator import itemgetter
ventas = [
{"region": "EU", "monto": 100},
{"region": "EU", "monto": 200},
{"region": "US", "monto": 150},
]
# groupby exige que los datos estén ordenados por la clave de agrupamiento
ventas_ordenadas = sorted(ventas, key=itemgetter("region"))
for region, items in groupby(ventas_ordenadas, key=itemgetter("region")):
total = sum(v["monto"] for v in items)
print(region, total)
Reducción con operadores. Todas las agregaciones clásicas pueden reducirse con reduce y un operador:
from functools import reduce
from operator import add, mul, and_, or_
# Concatenación de listas (evitar en grandes volúmenes)
reduce(add, [[1, 2], [3, 4], [5, 6]]) # [1, 2, 3, 4, 5, 6]
# Intersección de sets
reduce(and_, [{1, 2, 3}, {2, 3, 4}, {3, 4, 5}]) # {3}
# Unión de sets
reduce(or_, [{1, 2}, {2, 3}, {3, 4}]) # {1, 2, 3, 4}
Para la suma numérica, sum() sigue siendo más idiomático y rápido que reduce(add, ...). Para el producto, math.prod() existe desde Python 3.8.
operator vs lambda: la medición
La ganancia de rendimiento viene de la implementación en C. Un pequeño benchmark sobre el orden de 100 000 diccionarios:
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")
En CPython 3.12 se observa por lo general una proporción de 1,2x a 1,5x a favor de itemgetter según la máquina. No es espectacular, pero es gratis, y el código queda más corto. En operaciones críticas dentro de bucles críticos (map, filter sobre millones de elementos), la ganancia se vuelve visible.
La otra dimensión es la serializabilidad con pickle. itemgetter("k") puede serializarse con pickle, mientras que una lambda no. Es importante al pasar una función de clave a un pool de procesos vía multiprocessing:
from multiprocessing import Pool
from operator import itemgetter
with Pool(4) as pool:
# Funciona
result = pool.map(itemgetter("k"), data)
# Falla: PicklingError sobre la lambda
# result = pool.map(lambda d: d["k"], data)
Cuándo no usar operator
operator no es una solución mágica. Cuando la lógica de extracción no es trivial (cálculo, condición, acceso condicional), una función con nombre sigue siendo más legible:
# Mal uso: ilegible
sorted(data, key=lambda x: itemgetter("score")(x) if x["activo"] else 0)
# Mejor: función nombrada
def clave_orden(x):
return x["score"] if x["activo"] else 0
sorted(data, key=clave_orden)
operator brilla en los casos simples y repetitivos. Más allá, una función dedicada mantiene el código claro.
Lo que conviene retener
La librería operator no transforma radicalmente el rendimiento de un programa, pero elimina toda una categoría de lambdas triviales que ensucian el código. itemgetter, attrgetter y methodcaller son las tres herramientas a integrar primero en el reflejo diario: vuelven las claves de orden y las pipelines funcionales autodocumentadas, ligeramente más rápidas, y serializables. Los operadores aritméticos y de comparación completan el abanico para los casos de reducción. Entender operator es aprender a expresar operaciones como valores de primera clase, una de las fortalezas infrautilizadas de Python.
