Nivel: Intermedio+
Tema: Caché, política LRU (Least Recently Used), dict ordenado, diseño de APIs, tests con pytest
Objetivo: Implementar una caché con capacidad limitada que expulsa el elemento menos recientemente usado cuando se llena.

En Python 3.7+ los
dictpreservan el orden de inserción. Usaremos ese comportamiento para simular LRU moviendo claves “usadas” al final.
Enunciado
Crea una clase LRUCache con:
Constructor
LRUCache(capacidad)
Reglas:
- Si
capacidadesNoneo no esint, lanceTypeError. - Si
capacidad<= 0, lanceValueError.
Métodos
get(clave)
- Devuelve el valor asociado a
clavesi existe. - Si no existe, devuelve
None. - Si existe, debe marcar esa clave como “recientemente usada” (moverla al final).
set(clave, valor)
- Inserta o actualiza el par
clave → valor. - Si la clave ya existe, actualiza el valor y marca como usada.
- Si se excede la capacidad, debe expulsar la clave menos recientemente usada.
clavedebe ser hashable; si no, lanceTypeError.
__len__()
- Devuelve el número de elementos en caché.
Ejemplos
cache = LRUCache(2)
cache.set("a", 1)
cache.set("b", 2)
cache.get("a") # 1 (ahora "a" es la más reciente)
cache.set("c", 3) # expulsa "b" (porque "b" es la menos usada)
cache.get("b") # None
cache.get("c") # 3
len(cache) # 2
Pistas
- Usa un
dictinterno:self._data = {} - Para mover una clave al final:
- lee el valor
- elimina la clave
- reinsertarla con el mismo valor
- Para expulsar la menos reciente:
- la primera clave del dict es la menos reciente
- obtén
next(iter(self._data))y elimínala
- Verifica hashability con
hash(clave).
Solución explicada (paso a paso)
- Validamos la capacidad.
set():- validar hashability
- si existe: borrar y reinsertar (mover al final)
- si no existe y está lleno: expulsar la primera clave
- insertar
get():- si existe: mover al final y devolver valor
- si no: devolver
None
__len__: retornarlen(self._data).
class LRUCache: """ LRU Cache simplificado usando dict ordenado (insertion-order). Política: - Cada get/set marca la clave como 'recientemente usada' moviéndola al final. - Si se excede la capacidad, expulsa la clave menos recientemente usada (la primera del dict). """ def __init__(self, capacidad: int): if capacidad is None or not isinstance(capacidad, int): raise TypeError("El parámetro 'capacidad' debe ser un entero (int).") if capacidad <= 0: raise ValueError("'capacidad' debe ser mayor que 0.") self.capacidad = capacidad self._data = {} def __len__(self) -> int: return len(self._data) def _asegurar_hashable(self, clave): try: hash(clave) except TypeError as exc: raise TypeError("La 'clave' debe ser hashable (p. ej. str, int, tuple).") from exc def get(self, clave): self._asegurar_hashable(clave) if clave not in self._data: return None valor = self._data[clave] # mover al final del self._data[clave] self._data[clave] = valor return valor def set(self, clave, valor) -> None: self._asegurar_hashable(clave) if clave in self._data: # actualizar y mover al final del self._data[clave] self._data[clave] = valor return # nueva clave if len(self._data) >= self.capacidad: # expulsar la menos reciente (primera insertada) clave_menos_reciente = next(iter(self._data)) del self._data[clave_menos_reciente] self._data[clave] = valor
import pytestfrom reto_25_lru_cache import LRUCachedef test_lru_basico_expulsiona_menos_reciente(): cache = LRUCache(2) cache.set("a", 1) cache.set("b", 2) assert cache.get("a") == 1 # "a" pasa a ser la más reciente cache.set("c", 3) # debe expulsar "b" assert cache.get("b") is None assert cache.get("c") == 3 assert len(cache) == 2def test_set_actualiza_y_mueve_al_final(): cache = LRUCache(2) cache.set("a", 1) cache.set("b", 2) cache.set("a", 10) # actualizar "a" la vuelve reciente cache.set("c", 3) # debe expulsar "b" assert cache.get("a") == 10 assert cache.get("b") is None assert cache.get("c") == 3def test_get_inexistente_devuelve_none(): cache = LRUCache(2) assert cache.get("x") is Nonedef test_capacidad_invalida(): with pytest.raises(TypeError): LRUCache(None) with pytest.raises(TypeError): LRUCache("2") with pytest.raises(ValueError): LRUCache(0) with pytest.raises(ValueError): LRUCache(-1)def test_clave_no_hashable_lanza_typeerror(): cache = LRUCache(2) with pytest.raises(TypeError): cache.set(["lista"], 1) with pytest.raises(TypeError): cache.get(["lista"])
Ejecuta:
pytest -q
Variantes para subir de nivel (opcional)
- Método
items()para inspeccionar el estado del caché (para debugging) - Soportar
pop(clave)explícito - Añadir estadísticas (hits/misses/evictions)
- Implementar LRU con lista doblemente enlazada (más “clásico”, O(1) real para mover nodos)
Lo que aprendiste
- Qué es LRU y por qué se usa en sistemas reales
- Cómo usar el orden de inserción de
dictcomo estructura base - Cómo diseñar una clase pequeña con contrato claro
- Cómo testear expulsión y actualización correctamente
Accede al código completo y a los tests en GitHub para ejecutar y modificar la solución localmente.
Si quieres seguir con retos “tipo producción”, el siguiente recomendado es:
Reto #26 — Rate limiter (token bucket simple) para practicar tiempo, estado y lógica defensiva.