El módulo functools de la biblioteca estándar ofrece herramientas de programación funcional para Python. Las cuatro más usadas en la práctica son @lru_cache y @cache para memoización, partial() para fijar argumentos, reduce() para acumular y @wraps para decoradores correctos.
@lru_cache y @cache: memoización automática
@lru_cache(maxsize=N) cachea los últimos N resultados. @cache (Python 3.9+) es equivalente a lru_cache(maxsize=None) y crece sin límite:
import functools
import time
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
inicio = time.perf_counter()
print(fibonacci(40)) # 102334155
print(f"Tiempo: {time.perf_counter() - inicio:.6f}s") # < 0.001s
# Sin cache, fibonacci(40) requiere ~2^40 llamadas; con cache, solo 41
print(fibonacci.cache_info())
# CacheInfo(hits=38, misses=41, maxsize=128, currsize=41)
# Invalida el caché si los datos cambian
fibonacci.cache_clear()
@cache para funciones puras sin límite de tamaño
import functools
@functools.cache
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(10)) # 3628800
print(factorial(5)) # 120 ya estaba en caché desde factorial(10)
partial: fijar argumentos de una función
functools.partial(func, *args, **kwargs) devuelve una nueva función con algunos argumentos ya fijados:
import functools
# Caso 1: especializar una función genérica
def potencia(base, exponente):
return base ** exponente
cuadrado = functools.partial(potencia, exponente=2)
cubo = functools.partial(potencia, exponente=3)
print(cuadrado(5)) # 25
print(cubo(3)) # 27
# Caso 2: adaptar una función a una interfaz específica
import json
dumps_bonito = functools.partial(json.dumps, indent=2, ensure_ascii=False)
print(dumps_bonito({"mensaje": "Hola, España"}))
# Caso 3: callbacks con parámetros extra
def procesar(item, config, verbose=False):
pass
mi_procesador = functools.partial(procesar, config={"modo": "rapido"}, verbose=True)
# Ahora mi_procesador solo necesita el item
lista_items = ["a", "b", "c"]
resultados = list(map(mi_procesador, lista_items))
reduce: acumular valores
import functools import operator # reduce(función, iterable, valor_inicial_opcional) # acumula de izquierda a derecha # Producto de una lista numeros = [1, 2, 3, 4, 5] producto = functools.reduce(operator.mul, numeros) print(producto) # 120 # Equivalente a: ((((1 * 2) * 3) * 4) * 5) # Máximo sin max() maximo = functools.reduce(lambda a, b: a if a > b else b, numeros) print(maximo) # 5 # Aplanar lista de listas listas = [[1, 2], [3, 4], [5, 6]] plana = functools.reduce(lambda a, b: a + b, listas) print(plana) # [1, 2, 3, 4, 5, 6] # (aunque chain.from_iterable es más eficiente)
@wraps: decoradores que preservan metadatos
import functools
# Sin @wraps: el decorador oculta el nombre y docstring de la función
def logging_decorator(func):
def wrapper(*args, **kwargs):
print(f"Llamando {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logging_decorator
def mi_funcion():
"""Docstring de mi función."""
pass
print(mi_funcion.__name__) # 'wrapper' incorrecto
print(mi_funcion.__doc__) # None perdido
# Con @wraps: correcto
def logging_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Llamando {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logging_decorator
def mi_funcion():
"""Docstring de mi función."""
pass
print(mi_funcion.__name__) # 'mi_funcion'
print(mi_funcion.__doc__) # 'Docstring de mi función.'
En la práctica diaria, @lru_cache y @cache son los más usados de functools. partial() es excelente para configurar funciones en tiempo de setup sin lambdas. @wraps es obligatorio en cualquier decorador que escribas.
