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.

Enunciado
Crea una función llamada memoize(func) que:
- Reciba una función
func. - Si
funcno es callable, lanceTypeError. - 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
- cachee resultados basados en los argumentos posicionales (
- Restricciones:
- Solo debe aceptar argumentos hashables (si no lo son, debe lanzar
TypeErrorcon un mensaje claro). - Debe preservar el nombre y docstring de la función original (usa
functools.wraps).
- Solo debe aceptar argumentos hashables (si no lo son, debe lanzar
Ejemplos
@memoize
def cuadrado(n):
return n * n
cuadrado(10) # calcula
cuadrado(10) # devuelve desde caché
Pistas
- Para construir una clave de caché, combina:
args(tupla)kwargsordenados:tuple(sorted(kwargs.items()))
functools.wrapsmantiene metadata (__name__,__doc__).- Para detectar no-hashables, puedes intentar
hash(clave).
Solución explicada (paso a paso)
- Validar que
funcsea callable. - Crear un diccionario
cache = {}. - 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
- Devolver
wrapperdecorada conwraps.
Python
from functools import wrapsdef 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 pytestfrom reto_24_memoize import memoizedef 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"] == 2def 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"] == 1def 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)
- Soportar tamaño máximo de caché (LRU simple)
- Cache por tiempo (TTL: expira después de N segundos)
- Stats del caché (hits/misses)
- 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.