Aprende leyendo en orden

itertools y functools — Recetas de iteración y composición de funciones

Aprende con ejemplos las recetas de iteración combinations / product / chain / accumulate, fijar argumentos con partial, y acelerar recursión memoizando con @lru_cache.

itertools es un conjunto de funciones que construyen nuevos iterables a partir de otros existentes (combinaciones, concatenación, acumulación), y functools es un conjunto de funciones que transforman la propia función (fijar argumentos, memoización). Ambos pueden reemplazar patrones de bucle for + seguimiento de estado en una sola línea, reduciendo drásticamente tu código.

Iterables — valores sobre los que puedes iterar

La mayoría de funciones de itertools toman un "iterable" como entrada y devuelven un nuevo iterable, así que vale la pena fijar primero qué es un iterable. Un iterable (lo que puedes escribir a la derecha de for x in ___:) es cualquier cosa que puedas pasar a un bucle for, a list(...), a sum(...) o a map(...). list / tuple / str / dict / set / range, además de los generadores que tú escribes (funciones que usan yield), todos se comportan como iterables en Python.

Iterables comunes
list[1, 2, 3]tuple(1, 2, 3)str"abc"dict{"a": 1}set{1, 2, 3}rangerange(5)
Lo que puedes poner detrás de for x in es un iterable. Tipos integrados como list / tuple / str / dict / set / range son iterables, y también lo son los generadores propios construidos con yield.

itertools — kit de combinación e iteración

itertools es un módulo que reúne funciones que construyen nuevos iterables a partir de otros existentes. Las cuatro que más usarás son combinations (combinaciones sin orden), product (producto cartesiano sobre varios conjuntos), chain (concatenar iterables en uno) y accumulate (totales o productos acumulados). Conocer estas cuatro te permite reescribir dobles bucles for y bucles con estado mucho más limpiamente.

Las cuatro funciones principales de itertools
combinationsCombinaciones sin ordenproductTodos los pares entre conjuntoschainConcatenar iterablesaccumulateSuma / producto acumulado
combinations para pares / tríos sin orden, product para todos los pares entre conjuntos, chain para concatenar iterables, y accumulate para estado acumulado al recorrer. Las cuatro devuelven iteradores, así que envuélvelas con list(...) para materializar.
FunciónEntrada → SalidaEjemplo
combinations(it, k)Combinaciones de k elementos (sin orden)[A,B,C] → (A,B), (A,C), (B,C)
permutations(it, k)Permutaciones de k elementos (con orden)[A,B] → (A,B), (B,A)
product(*its)Producto cartesiano entre conjuntos[S,M] × [red,blue] → 4 combos
chain(*its)Concatenar varios iterables[1,2] + [3,4] → [1,2,3,4]
accumulate(it)Acumular desde la izquierda (suma por defecto)[10,20,30] → [10, 30, 60]

Lista pares de dos bebidas elegidas de tres opciones y todas las SKUs talla × color. combinations da pares sin orden, product da el producto cartesiano entre conjuntos.

① Importa combinations y product desde itertools

② Desde las bebidas ["coffee", "tea", "juice"], construye pares de 2 con combinations, materializa, e imprime como Pares: ◯

③ Construye los todos los combos de tallas ["S", "M", "L"] × colores ["red", "blue"] con product, materializa, e imprime como Todos los SKUs: ◯

(Si tu código se ejecuta correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

Concatena los datos de ventas de dos días y calcula el total acumulado. chain junta varios iterables en uno y accumulate recorre desde la izquierda llevando un valor acumulado.

① Importa chain y accumulate desde itertools

② Concatena las ventas del día 1 [120, 80] y del día 2 [200, 150, 90] con chain, materializa e imprime como Todas las ventas: ◯

③ Aplica accumulate al resultado concatenado, materializa e imprime como Total acumulado: ◯

Editor Python

Ejecutar el código para ver el resultado

functools.partial — fija argumentos por adelantado

functools.partial construye una nueva función con algunos argumentos ya fijados. Cuando necesitas pasar una función con argumentos fijados a un callback o a una función de orden superior, esto te ahorra escribir wrappers de una línea como def wrapper(...): .... En cualquier sitio donde llames a la misma función repetidamente con ligeras variaciones de argumentos, partial reduce el ruido del wrapper y hace el código más legible.

from functools import partial

def format_with_unit(price, unit):
    return f"{price}{unit}"

# Construye una nueva función con la unidad "USD" prefijada
to_usd = partial(format_with_unit, unit="USD")

# Aplica a precios individuales — solo hay que pasar price ahora
print(to_usd(100))   # 100USD
print(to_usd(200))   # 200USD
print(to_usd(300))   # 300USD
Cómo funciona partial
power(base, exp)Devuelve base ** exppartial(power, exp=2)Fija exp=2square(3) → 9square(5) → 25
partial(función, arg=valor) devuelve una nueva función con algunos argumentos fijados. Fija exp=2 en power(base, exp) y obtienes una función square que solo necesita base.

Usa partial para fijar exp en power(base, exp) y derivar funciones dedicadas square / cube.

① Importa partial desde functools

② Define def power(base, exp): que devuelva `base ** exp

③ Con partial, construye una función `square` con `exp=2` fijado. Llama a square(3) y square(5) e imprime como 3 al cuadrado: ◯ / 5 al cuadrado: ◯

④ Con partial, construye una función `cube` con `exp=3` fijado. Llama a cube(2) e imprime como 2 al cubo: ◯

Editor Python

Ejecutar el código para ver el resultado

functools.lru_cache — cachea resultados con memoización

"Quiero reutilizar el resultado de una función costosa que se llama repetidamente con los mismos argumentos" — surge siempre que las salidas de una función vienen determinadas por sus entradas y la velocidad importa. Hacerlo a mano significa gestionar tu propio dict de caché, pero @lru_cache lo hace en una sola línea con un decorador.

@lru_cache es un decorador que cachea los valores de retorno de una función. Cuando se llama de nuevo con los mismos argumentos, devuelve el valor cacheado directamente, saltándose el recálculo. LRU (Least Recently Used — descarta las entradas no usadas durante más tiempo) descarta entradas antiguas de la caché así que puedes limitar el uso de memoria con algo como maxsize=128.

Cómo funciona lru_cache
fib(30) primera vezEjecuta cuerpoCachea resultado+ devuelve valorfib(30) segunda vezLee de la caché→ Retorno instantáneo
La primera llamada falla en la caché, así que el cuerpo de la función se ejecuta y el resultado se guarda como (args → valor de retorno). Las llamadas siguientes con los mismos args devuelven inmediatamente desde la caché — el cuerpo nunca se ejecuta.

No lo apliques a funciones con efectos secundarios

@lru_cache asume "mismos argumentos → mismo valor de retorno". Aplícalo a funciones con efectos secundarios (leer archivos / escribir en una BD / devolver la hora actual) o cuyo resultado varíe para las mismas entradas, y tendrás un bug donde el primer resultado vuelve para siempre. Aplícalo solo a funciones puras (misma entrada → misma salida, sin efectos secundarios).

Acelera un Fibonacci recursivo con @lru_cache. Incluso en fib(30) la cuenta de llamadas es enorme sin caché, pero con el decorador es instantáneo.

① Importa lru_cache desde functools

② Define una función fib(n) decorada con @lru_cache(maxsize=128) (devuelve n si n < 2, en caso contrario devuelve fib(n-1) + fib(n-2))

③ Imprime fib(30) como fib(30): ◯

④ Imprime fib(50) como fib(50): ◯ (sin caché, esto no terminaría en un tiempo razonable)

⑤ Imprime maxsize: ◯ usando fib.cache_info().maxsize

Editor Python

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Cuál es la forma más concisa de construir el producto cartesiano de [1, 2, 3] y [A, B]?

Pregunta 2¿Qué devuelve partial(f, x=10)?

Pregunta 3¿Por qué @lru_cache acelera un Fibonacci recursivo ingenuo?