Algunos objetos de Python contienen valores baratos de almacenar y otros que requieren leer archivos, procesar colecciones, consultar servicios o repetir cálculos. Cuando un resultado depende solamente del estado del objeto y puede reutilizarse, calcularlo en cada acceso desperdicia tiempo. cached_property en Python resuelve este caso al convertir un método en un atributo que se calcula una sola vez y se guarda en la instancia.
En esta guía aprenderás a usar functools.cached_property, invalidar el valor almacenado, evitar resultados desactualizados, tratar accesos concurrentes, probar el comportamiento y elegir entre cached_property, property y lru_cache. El recurso se relaciona directamente con las clases y objetos en Python, las funciones, los descriptors y las dataclasses.
¿Qué es cached_property?
cached_property es un descriptor de la biblioteca estándar disponible en el módulo functools. Durante el primer acceso ejecuta el método decorado, guarda el resultado en el diccionario de la instancia y lo devuelve. Los accesos posteriores encuentran el atributo almacenado directamente, sin volver a llamar al método.
Esta es la diferencia principal frente a una property normal. El getter de una propiedad se ejecuta cada vez que se lee el atributo. Una propiedad en caché cambia memoria por procesamiento: cada instancia conserva su propio resultado hasta que el objeto desaparece o el atributo se elimina.
La documentación oficial de cached_property explica el almacenamiento en la instancia, los requisitos de __dict__ y los accesos concurrentes. La guía oficial de descriptors ayuda a comprender por qué el descriptor controla la primera consulta y después deja que el atributo guardado responda por sí mismo.
Primer ejemplo
Imaginemos una clase que representa un informe cargado desde un archivo. Leer y transformar los datos puede ser costoso, pero el contenido procesado probablemente se utilizará varias veces:
from functools import cached_property
from pathlib import Path
import json
class Informe:
def __init__(self, ruta: str):
self.ruta = Path(ruta)
@cached_property
def datos(self) -> list[dict]:
print("Leyendo y procesando el archivo...")
texto = self.ruta.read_text(encoding="utf-8")
return json.loads(texto)
informe = Informe("ventas.json")
print(len(informe.datos))
print(informe.datos[0])El mensaje aparece solamente durante el primer acceso. Después, datos existe en el __dict__ de la instancia:
print(informe.__dict__)
# {'ruta': Path('ventas.json'), 'datos': [...]}La caché pertenece al objeto y no a toda la clase. Dos instancias de Informe calculan y guardan valores independientes.
Cómo invalidar el valor almacenado
Elimina el atributo para vaciar la caché. El siguiente acceso ejecutará nuevamente el método:
del informe.datos
print(informe.datos)Este comportamiento permite implementar una invalidación explícita. Cuando otro atributo cambia y vuelve incorrecto el resultado anterior, elimina la entrada guardada.
class Informe:
def __init__(self, ruta: str):
self._ruta = Path(ruta)
@property
def ruta(self) -> Path:
return self._ruta
@ruta.setter
def ruta(self, nueva_ruta: str) -> None:
self._ruta = Path(nueva_ruta)
self.__dict__.pop("datos", None)
@cached_property
def datos(self) -> list[dict]:
texto = self._ruta.read_text(encoding="utf-8")
return json.loads(texto)El método pop evita un error cuando el valor todavía no fue calculado. La clave debe coincidir exactamente con el nombre del método decorado.
El mayor riesgo: una caché desactualizada
cached_property no sabe qué campos influyen en el cálculo. Si el estado cambia, el decorador no vuelve a calcular automáticamente. Funciona mejor con objetos inmutables, datos cargados una sola vez o resultados cuya validez dura toda la vida de la instancia.
Observa una clase de pedido:
from functools import cached_property
class Pedido:
def __init__(self, elementos):
self.elementos = elementos
@cached_property
def total(self):
return sum(
elemento["precio"] * elemento["cantidad"]
for elemento in self.elementos
)Si alguien modifica pedido.elementos después del primer acceso a total, el importe guardado quedará incorrecto. Las principales soluciones son convertir los datos de origen en inmutables, invalidar la caché en cada punto de modificación o no usar caché para esa propiedad. La última opción suele ser más segura cuando los cambios son frecuentes o difíciles de controlar.
Los resultados mutables requieren cuidado
Cada acceso devuelve el mismo objeto almacenado. Si la propiedad retorna una lista o un diccionario y un consumidor lo modifica, esa modificación pasa a formar parte de la caché:
datos = informe.datos
datos.clear()
print(informe.datos)Cuando los consumidores no deberían modificar el resultado, devuelve una tupla, un frozenset, una estructura inmutable o una copia controlada. Otra alternativa es mantener una propiedad privada en caché y exponer un método que entregue una copia.
cached_property con dataclasses
El decorador funciona naturalmente con dataclasses comunes siempre que la instancia tenga __dict__:
from dataclasses import dataclass
from functools import cached_property
from statistics import mean
@dataclass
class Curso:
nombre: str
notas: tuple[float, ...]
@cached_property
def promedio(self) -> float:
if not self.notas:
return 0.0
return mean(self.notas)Como las notas están almacenadas en una tupla, el promedio calculado permanece coherente. Una dataclass congelada puede conservar el diccionario interno que utiliza el descriptor, pero conviene probar la estructura exacta del proyecto y no asumir que todos los mecanismos de inmutabilidad se comportan igual.
Limitaciones con __slots__
cached_property necesita un __dict__ mutable para guardar el resultado. Una clase que utiliza __slots__ sin diccionario no ofrece ese espacio:
class Punto:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = yEn esta situación, usa una propiedad normal, reserva un slot específico para el valor calculado o implementa una caché externa. Añadir "__dict__" a __slots__ recupera la compatibilidad, pero también reduce parte del ahorro de memoria que motivó el uso de slots.
Concurrencia y ejecución duplicada
No consideres cached_property una garantía de que el método se ejecutará exactamente una vez entre varias threads. Dos accesos simultáneos pueden comenzar el cálculo antes de que uno consiga guardar el resultado. Normalmente esto es aceptable cuando el método es idempotente y no tiene efectos secundarios.
Si una ejecución duplicada puede provocar cobros, escrituras, cambios de estado u otro problema, protege la operación crítica con un lock de la instancia:
from functools import cached_property
from threading import Lock
class Cliente:
def __init__(self, api):
self.api = api
self._perfil_lock = Lock()
@cached_property
def perfil(self):
with self._perfil_lock:
return self.api.obtener_perfil()Este patrón evita llamadas superpuestas mediante la misma instancia. Sin embargo, una aplicación muy concurrente puede necesitar una estrategia más completa con timeout, control de errores e invalidación explícita.
¿Qué ocurre cuando falla el cálculo?
Si el método lanza una excepción, no se guarda ningún resultado válido. Un acceso posterior intentará ejecutar el método otra vez. Esto puede ser útil para fallos transitorios, pero también puede repetir una operación costosa indefinidamente.
@cached_property
def configuracion(self):
if not self.ruta.exists():
raise FileNotFoundError(self.ruta)
return cargar_configuracion(self.ruta)Decide si conviene reintentar en cada acceso. Para integraciones remotas, quizá sea mejor aplicar reintentos controlados, registrar el fallo o mantener un estado separado. No conviertas excepciones en valores silenciosos solamente para llenar la caché.
cached_property o property
Elige property cuando el valor debe reflejar siempre el estado actual, el cálculo es barato o la validación debe ejecutarse en cada lectura. Elige cached_property cuando el cálculo tiene un costo relevante, el resultado permanece estable y la instancia puede almacenarlo.
class Circulo:
def __init__(self, radio):
self.radio = radio
@property
def diametro(self):
return self.radio * 2Guardar un cálculo tan pequeño aporta poco beneficio. El costo de memoria y las reglas de invalidación serían mayores que la multiplicación.
cached_property o lru_cache
lru_cache almacena resultados para combinaciones de argumentos de una función. Es adecuado para funciones puras llamadas repetidamente. Cuando se usa en métodos, puede incluir self en la clave, lo que exige una instancia hashable y puede mantener objetos vivos mientras existan entradas en la caché.
cached_property es más simple cuando existe un único valor sin argumentos por instancia. El resultado acompaña la vida del objeto y puede invalidarse con del. En cambio, lru_cache ofrece límite de entradas, estadísticas de aciertos y limpieza global por función.
Cómo probar el comportamiento
Una prueba útil debe comprobar el resultado, la cantidad de ejecuciones y la invalidación:
from functools import cached_property
class Ejemplo:
def __init__(self):
self.llamadas = 0
@cached_property
def valor(self):
self.llamadas += 1
return 42
def test_cached_property():
obj = Ejemplo()
assert obj.valor == 42
assert obj.valor == 42
assert obj.llamadas == 1
del obj.valor
assert obj.valor == 42
assert obj.llamadas == 2En objetos mutables, comprueba también que cada setter relevante elimina la clave correcta de __dict__.
Consumo de memoria
Cada propiedad calculada añade un valor al diccionario de la instancia. Unas pocas entradas suelen ser insignificantes, pero miles de objetos de larga vida con resultados grandes pueden consumir mucha memoria. El decorador también puede afectar los diccionarios con claves compartidas que CPython utiliza para ahorrar espacio.
Mide antes de optimizar. Si existen muchas instancias, los resultados son grandes o pocas instancias usan la propiedad, una caché externa con límite o un componente explícito de carga diferida puede ofrecer más control.
Buenas prácticas
- Guarda solamente cálculos con un costo relevante.
- Prefiere resultados estables durante la vida de la instancia.
- Documenta qué cambios requieren invalidación.
- Evita efectos secundarios dentro del método decorado.
- Protege operaciones no idempotentes en escenarios concurrentes.
- Ten cuidado al devolver colecciones mutables.
- Prueba el primer acceso, la reutilización, el fallo y la eliminación.
- Confirma la presencia de
__dict__al usar slots o tipos especiales.
Conclusión
cached_property en Python es una herramienta pequeña y valiosa para objetos con valores derivados costosos y estables. El primer acceso calcula el resultado; los accesos posteriores lo tratan como un atributo común de la instancia. La API permanece sencilla y evita procesamiento repetido.
La optimización solamente es segura cuando las reglas de validez están claras. Antes de aplicar el decorador, identifica los campos de origen, si pueden cambiar, cómo se invalidará el valor y si existen accesos concurrentes. Cuando esas respuestas son simples, cached_property resulta legible y eficaz. Cuando no lo son, una propiedad normal o una capa explícita de caché suele ser más segura.






