SolveConPython

Python Reto #24 — Cache simple (memoization) para acelerar funciones

Nivel: Intermedio+
Tema: Rendimiento, diccionarios como caché, diseño de APIs, funciones puras, tests con pytest
Objetivo: Implementar memoization (caché) para evitar cálculos repetidos, una técnica muy útil para optimización.

Reto #24 Cache simple (memoization) para acelerar funciones
Reto #24 Cache simple (memoization) para acelerar funciones

Enunciado

Crea una función llamada memoize(func) que:

  1. Reciba una función func.
  2. Si func no es callable, lance TypeError.
  3. Devuelva una nueva función (wrapper) que:
    • cachee resultados basados en los argumentos posicionales (*args) y nombrados (**kwargs)
    • si se vuelve a llamar con los mismos argumentos, devuelva el resultado cacheado
  4. Restricciones:
    • Solo debe aceptar argumentos hashables (si no lo son, debe lanzar TypeError con un mensaje claro).
    • Debe preservar el nombre y docstring de la función original (usa functools.wraps).

Ejemplos

@memoize
def cuadrado(n):
return n * n

cuadrado(10) # calcula
cuadrado(10) # devuelve desde caché

Pistas

  1. Para construir una clave de caché, combina:
    • args (tupla)
    • kwargs ordenados: tuple(sorted(kwargs.items()))
  2. functools.wraps mantiene metadata (__name__, __doc__).
  3. Para detectar no-hashables, puedes intentar hash(clave).

Solución explicada (paso a paso)

  1. Validar que func sea callable.
  2. Crear un diccionario cache = {}.
  3. Definir la función wrapper(*args, **kwargs):
    • construir una clave única con args + kwargs ordenados
    • verificar que la clave sea hashable
    • si existe en cache: devolver el valor
    • si no: ejecutar func, guardar y devolver
  4. Devolver wrapper decorada con wraps.

Python
from functools import wraps
def memoize(func):
"""
Decorador que cachea resultados de una función según args y kwargs.
Restricciones:
- Solo admite args/kwargs hashables
"""
if not callable(func):
raise TypeError("El parámetro 'func' debe ser una función (callable).")
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# kwargs: ordenados para que el orden no afecte la clave
clave = (args, tuple(sorted(kwargs.items())))
try:
hash(clave)
except TypeError as exc:
raise TypeError("Los argumentos deben ser hashables para usar memoize().") from exc
if clave in cache:
return cache[clave]
resultado = func(*args, **kwargs)
cache[clave] = resultado
return resultado
return wrapper

Python
import pytest
from reto_24_memoize import memoize
def test_memoize_cachea_resultados():
llamadas = {"n": 0}
@memoize
def f(x):
llamadas["n"] += 1
return x * 2
assert f(10) == 20
assert f(10) == 20
assert llamadas["n"] == 1 # solo se ejecutó una vez
assert f(11) == 22
assert llamadas["n"] == 2
def test_kwargs_orden_no_importa():
llamadas = {"n": 0}
@memoize
def g(a=1, b=2):
llamadas["n"] += 1
return a + b
assert g(a=1, b=2) == 3
assert g(b=2, a=1) == 3
assert llamadas["n"] == 1
def test_func_no_callable():
with pytest.raises(TypeError):
memoize(123)
def test_argumentos_no_hashables_lanzan_typeerror():
@memoize
def h(x):
return x
with pytest.raises(TypeError):
h([1, 2, 3]) # list no es hashable

Ejecuta:

  • pytest -q

Variantes para subir de nivel (opcional)

  1. Soportar tamaño máximo de caché (LRU simple)
  2. Cache por tiempo (TTL: expira después de N segundos)
  3. Stats del caché (hits/misses)
  4. Comparar con functools.lru_cache (cuándo usar el estándar)

Lo que aprendiste

  • Qué es memoization y por qué mejora rendimiento
  • Cómo construir claves de caché estables con args/kwargs
  • Importancia de hashability en diccionarios
  • Cómo testear comportamiento con “contadores de llamadas”

Accede al código completo y a los tests en GitHub para ejecutar y modificar la solución localmente.

El siguiente reto recomendado: Reto #25 — LRU Cache simplificado (capacidad máxima) para dar un paso más en optimización y estructuras de datos.