cached_property en Python: caché en objetos

Publicado el: 25/07/2026
Tempo de leitura: 7 minutos
Código Python con cached_property para guardar cálculos costosos

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 = y

En 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 * 2

Guardar 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 == 2

En 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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Código Python con funciones especializadas por tipo
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    singledispatch en Python: funciones por tipo

    Aprende singledispatch en Python para crear funciones por tipo, reducir cadenas isinstance y organizar polimorfismo extensible con ejemplos.

    Ler mais

    Tempo de leitura: 7 minutos
    25/07/2026
    Código Python y atributos
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Descriptors en Python: guía práctica

    Aprende descriptors en Python con __get__, __set__, validación, property, almacenamiento por instancia, pruebas, herencia y buenas prácticas.

    Ler mais

    Tempo de leitura: 7 minutos
    22/07/2026
    Leitura de arquivos grandes em Python sem travar o sistema
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo leer archivos gigantes con Python sin bloquear el sistema

    Aprende a leer archivos gigantes con Python usando iteración, bloques, generadores, chunks de Pandas, compresión y puntos de control.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Herança múltipla em Python sem causar problemas no código
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Herencia múltiple en Python: MRO, super() y mixins

    Aprende herencia múltiple en Python con MRO, super(), mixins, problema del diamante, inicializadores cooperativos, composición y pruebas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Criando instalador EXE com ícone personalizado em Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo crear un ejecutable EXE con icono personalizado en Python

    Crea un EXE de Python con PyInstaller, icono ICO, onefile, windowed, archivos de datos, SPEC, rutas seguras, pruebas y distribución.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Uso do super em Python para resolver problemas de herança
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo usar super() en Python sin errores de herencia

    Aprende a usar super() en Python con __init__, MRO, herencia múltiple, mixins, argumentos cooperativos, pruebas y composición.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026