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 frame que 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 (como operator.add o itemgetter) no necesita un frame Python, se ejecuta directamente dentro de la VM. Ese ahorro, multiplicado por millones de iteraciones, es lo que vuelve a operator mensurable.

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ónFunción
a + boperator.add(a, b)
a - boperator.sub(a, b)
a * boperator.mul(a, b)
a / boperator.truediv(a, b)
a // boperator.floordiv(a, b)
a % boperator.mod(a, b)
a ** boperator.pow(a, b)
a < boperator.lt(a, b)
a <= boperator.le(a, b)
a == boperator.eq(a, b)
a > boperator.gt(a, b)
a is boperator.is_(a, b)
not aoperator.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.