functools.lru_cache es un decorador de la biblioteca estándar que memoriza el resultado de una función. Cuando la misma función vuelve a recibir los mismos argumentos, Python devuelve el valor guardado en lugar de repetir el cálculo. Esta técnica, conocida como memoización, puede acelerar algoritmos recursivos, consultas repetidas y transformaciones costosas con muy poco código.
Para comprender el patrón conviene conocer los decoradores en Python, la recursión, la medición con timeit y el análisis de cuellos de botella mediante cProfile.
El problema de calcular lo mismo varias veces
Una implementación recursiva ingenua de Fibonacci repite una gran cantidad de trabajo:
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35))Cada llamada crea otras llamadas que vuelven a calcular valores ya conocidos. El tiempo crece rápidamente.
Añadir lru_cache
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35))La primera vez que se calcula fibonacci(20), el resultado queda almacenado. Las llamadas posteriores con el argumento 20 recuperan ese valor.
Qué significa LRU
LRU significa “Least Recently Used”, o menos recientemente usado. Cuando la caché alcanza su límite, elimina primero las entradas que llevan más tiempo sin utilizarse. Esto permite controlar el consumo de memoria.
@lru_cache(maxsize=128)
def procesar(valor):
...El valor predeterminado de maxsize es 128. Con maxsize=None la caché puede crecer sin límite, por lo que solo debe utilizarse cuando el conjunto de argumentos posibles sea pequeño y conocido.
Los argumentos deben ser hashables
La caché utiliza los argumentos como claves. Por eso acepta enteros, cadenas, tuplas y otros objetos hashables, pero no listas o diccionarios mutables:
from functools import lru_cache
@lru_cache
def total(numeros):
return sum(numeros)
print(total((10, 20, 30)))Si necesitas recibir una lista, conviértela a tupla antes de llamar a la función o crea una capa externa:
@lru_cache
def _total_cache(numeros):
return sum(numeros)
def total(numeros):
return _total_cache(tuple(numeros))Inspeccionar el rendimiento de la caché
cache_info() muestra aciertos, fallos, tamaño máximo y entradas actuales:
print(fibonacci.cache_info())Un resultado puede verse así:
CacheInfo(hits=33, misses=36, maxsize=None, currsize=36)Los hits indican llamadas resueltas desde la caché. Los misses representan cálculos reales.
Vaciar la caché
Cuando cambian los datos subyacentes, utiliza:
fibonacci.cache_clear()En una aplicación real, borrar toda la caché puede ser costoso, pero evita devolver resultados obsoletos. Diseña una política clara de invalidación.
Parámetro typed
Con typed=True, algunos argumentos de tipos distintos se almacenan por separado:
@lru_cache(maxsize=64, typed=True)
def convertir(valor):
return str(valor)Esto puede ser relevante cuando 1, 1.0 y otros valores equivalentes deben producir comportamientos diferentes.
Ejemplo con lectura de configuración
from functools import lru_cache
import json
from pathlib import Path
@lru_cache(maxsize=1)
def cargar_configuracion():
ruta = Path("config.json")
return json.loads(ruta.read_text(encoding="utf-8"))La lectura del disco ocurre una vez. Sin embargo, si el archivo cambia, la función seguirá devolviendo el objeto almacenado hasta llamar a cargar_configuracion.cache_clear().
No caches funciones con efectos secundarios
Una función que envía un correo, guarda una compra o registra un pago no debe decorarse con lru_cache. La segunda llamada podría no ejecutar la operación:
# No hacer esto
@lru_cache
def registrar_pago(pedido_id):
guardar_en_base_de_datos(pedido_id)La caché funciona mejor con funciones puras: el resultado depende únicamente de los argumentos y la función no modifica estado externo.
Cuidado con valores mutables devueltos
La caché devuelve el mismo objeto almacenado. Si el resultado es una lista y alguien la modifica, las llamadas futuras verán la modificación:
@lru_cache
def obtener_etiquetas():
return ["python", "rendimiento"]Prefiere tuplas, objetos inmutables o devuelve una copia desde una capa pública.
Usar cache cuando no necesitas límite
Python también ofrece functools.cache, equivalente a una caché sin límite:
from functools import cache
@cache
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)Elige cache solo cuando el dominio de entradas esté controlado. Para entradas arbitrarias de usuarios, un límite evita crecimiento indefinido.
Caché en métodos de instancia
Al decorar un método, self forma parte de la clave:
from functools import lru_cache
class Conversor:
@lru_cache(maxsize=32)
def convertir(self, valor, tasa):
return valor * tasaLa caché mantiene una referencia a la instancia mientras existan entradas, lo que puede prolongar su vida en memoria. En objetos de corta duración, evalúa otra estrategia.
lru_cache y concurrencia
La estructura interna es segura frente a actualizaciones concurrentes básicas, pero dos hilos pueden calcular el mismo valor simultáneamente si ambos llegan antes de que exista la entrada. No asumas que el decorador convierte una operación en un bloqueo de exclusión mutua.
Medir antes y después
from timeit import timeit
sin_calentar = timeit("fibonacci(35)", globals=globals(), number=1)
con_cache = timeit("fibonacci(35)", globals=globals(), number=1000)
print(sin_calentar)
print(con_cache)Interpreta el resultado con cuidado: después de la primera llamada, las siguientes son aciertos de caché. Mide escenarios representativos y comprueba cache_info().
Cuándo evitar lru_cache
No lo uses si los resultados cambian constantemente, los argumentos tienen una cardinalidad enorme, el valor depende de hora, archivos o base de datos sin una invalidación clara, la función produce efectos secundarios o los resultados ocupan mucha memoria. Una caché incorrecta puede ser rápida y al mismo tiempo devolver datos equivocados.
La documentación oficial de lru_cache explica los parámetros, estadísticas y comportamiento en métodos. La documentación de functools.cache describe la alternativa sin límite.
Errores frecuentes
Los problemas comunes son pasar listas o diccionarios como argumentos, usar maxsize=None con entradas ilimitadas, cachear funciones con efectos secundarios, olvidar invalidar datos externos y devolver objetos mutables que después son modificados. Otro error es aplicar caché sin medir: si la función ya es rápida o casi nunca recibe argumentos repetidos, el beneficio puede ser mínimo.
Conclusión
lru_cache puede transformar una función repetitiva en una operación muy rápida, especialmente en recursión y cálculos puros. Define un tamaño razonable, utiliza argumentos hashables, inspecciona los aciertos y establece cuándo se vacía la caché. La memoización funciona mejor cuando la validez del resultado depende exclusivamente de los argumentos y el coste de recalcularlo es significativo.






