cached_property no Python: cache em objetos

Publicado em: 25/07/2026
Tempo de leitura: 7 minutos
Código Python usando cached_property para armazenar cálculos

Em classes Python, alguns valores são baratos de armazenar, enquanto outros exigem leitura de arquivos, processamento de coleções, consultas a serviços ou cálculos repetidos. Quando o resultado depende apenas do estado do objeto e pode ser reutilizado, recalculá-lo a cada acesso desperdiça tempo. O decorador cached_property no Python resolve esse cenário ao transformar um método em um atributo calculado uma única vez e armazenado na própria instância.

Neste guia, você aprenderá como usar functools.cached_property, invalidar o cache, evitar resultados desatualizados, lidar com concorrência, testar o comportamento e decidir entre cached_property, property e lru_cache. O recurso combina conceitos explicados nos artigos sobre programação orientada a objetos, decoradores em Python, descriptors e cache com lru_cache.

O que é cached_property?

cached_property é um descriptor da biblioteca padrão disponível no módulo functools. No primeiro acesso, ele executa o método decorado, guarda o resultado no dicionário da instância e devolve esse valor. Nos acessos seguintes, o atributo armazenado é encontrado diretamente, sem chamar novamente o método.

Essa diferença é importante. Uma property comum executa o getter sempre que o atributo é lido. Já uma cached_property troca cálculo por memória: o objeto mantém uma cópia do resultado enquanto existir ou até que o atributo seja removido.

A documentação oficial de cached_property descreve o armazenamento na instância, as limitações com __dict__ e o comportamento em acessos concorrentes. O guia oficial de descriptors ajuda a entender por que o decorador consegue controlar a primeira leitura e depois ceder lugar ao atributo gravado.

Primeiro exemplo

Imagine uma classe que representa um relatório carregado de um arquivo. A leitura e a transformação dos dados podem ser custosas, mas o conteúdo normalmente será usado várias vezes:

from functools import cached_property
from pathlib import Path
import json

class Relatorio:
    def __init__(self, caminho: str):
        self.caminho = Path(caminho)

    @cached_property
    def dados(self) -> list[dict]:
        print("Lendo e processando o arquivo...")
        texto = self.caminho.read_text(encoding="utf-8")
        return json.loads(texto)

relatorio = Relatorio("vendas.json")

print(len(relatorio.dados))
print(relatorio.dados[0])

A mensagem aparece apenas no primeiro acesso. Depois disso, dados passa a existir no __dict__ da instância:

print(relatorio.__dict__)
# {'caminho': Path('vendas.json'), 'dados': [...]}

O cache pertence ao objeto, não à classe inteira. Duas instâncias de Relatorio calculam e armazenam resultados independentes.

Como invalidar o valor armazenado

O cache pode ser removido com del. No próximo acesso, o método será executado novamente:

del relatorio.dados
print(relatorio.dados)

Essa operação é a base de uma estratégia de invalidação. Se uma alteração em outro atributo torna o resultado antigo incorreto, remova a propriedade em cache.

class Relatorio:
    def __init__(self, caminho: str):
        self._caminho = Path(caminho)

    @property
    def caminho(self) -> Path:
        return self._caminho

    @caminho.setter
    def caminho(self, novo_caminho: str) -> None:
        self._caminho = Path(novo_caminho)
        self.__dict__.pop("dados", None)

    @cached_property
    def dados(self) -> list[dict]:
        texto = self._caminho.read_text(encoding="utf-8")
        return json.loads(texto)

Usar pop evita um erro quando o valor ainda não foi calculado. O nome passado deve ser exatamente o nome do método decorado.

O maior risco: cache desatualizado

cached_property não sabe quais atributos influenciam o cálculo. Se o estado muda, o decorador não recalcula automaticamente. Por isso, ele funciona melhor em objetos imutáveis, dados carregados uma vez ou resultados cuja validade acompanha toda a vida da instância.

Considere uma classe de pedido:

from functools import cached_property

class Pedido:
    def __init__(self, itens):
        self.itens = itens

    @cached_property
    def total(self):
        return sum(item["preco"] * item["quantidade"] for item in self.itens)

Se alguém modificar pedido.itens depois do primeiro acesso a total, o valor ficará incorreto. Há três soluções principais: tornar os dados imutáveis, invalidar o cache em todos os pontos de alteração ou não usar cache nesse atributo. A terceira opção costuma ser a melhor quando as mudanças são frequentes e difíceis de controlar.

Resultados mutáveis exigem cuidado

O objeto devolvido é o mesmo em todos os acessos. Se a propriedade retorna uma lista ou dicionário e um consumidor o modifica, essa alteração passa a fazer parte do valor armazenado:

dados = relatorio.dados
dados.clear()

print(relatorio.dados)

Quando o resultado deve ser somente leitura, considere retornar uma tupla, um frozenset, uma estrutura imutável ou uma cópia controlada. Outra alternativa é manter a propriedade em cache privada e expor um método que devolve uma cópia.

cached_property com dataclasses

O decorador funciona bem com dataclasses comuns, desde que a instância possua __dict__:

from dataclasses import dataclass
from functools import cached_property
from statistics import mean

@dataclass
class Turma:
    nome: str
    notas: tuple[float, ...]

    @cached_property
    def media(self) -> float:
        if not self.notas:
            return 0.0
        return mean(self.notas)

Como a tupla de notas é imutável, o resultado permanece coerente. Mesmo em uma dataclass congelada, a implementação do descriptor pode armazenar o valor no dicionário interno da instância, mas vale testar o comportamento com a estrutura específica do projeto e evitar assumir que todos os mecanismos de imutabilidade funcionam da mesma maneira.

Limitações com __slots__

cached_property precisa de um __dict__ mutável para gravar o resultado. Uma classe que define __slots__ sem incluir __dict__ não oferece esse espaço:

class Ponto:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

Nesse caso, use uma propriedade normal, inclua um slot explícito para o valor calculado ou implemente um cache externo. Adicionar "__dict__" a __slots__ restaura a compatibilidade, mas também reduz parte da economia de memória que motivou o uso de slots.

Concorrência e execução duplicada

Não trate cached_property como uma garantia de execução única em múltiplas threads. Dois acessos simultâneos podem calcular o mesmo valor antes que uma das threads consiga armazená-lo. Normalmente isso é aceitável quando o método é idempotente e não possui efeitos colaterais.

Se executar duas vezes causar cobrança, gravação duplicada, mudança de estado ou outro problema, proteja a região crítica com um lock dentro da instância:

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.buscar_perfil()

Esse exemplo evita sobreposição durante a chamada, mas projetos altamente concorrentes podem precisar de uma estratégia mais completa, incluindo controle de falhas, timeout e invalidação.

O que acontece quando o cálculo falha?

Se o método lança uma exceção, nenhum resultado válido é armazenado. Um acesso posterior tentará executar o método novamente. Esse comportamento é útil para falhas transitórias, mas também pode repetir uma operação cara indefinidamente.

@cached_property
def configuracao(self):
    if not self.caminho.exists():
        raise FileNotFoundError(self.caminho)
    return carregar(self.caminho)

Decida se a repetição é desejável. Em integrações externas, talvez seja melhor aplicar tentativas controladas, registrar a falha ou armazenar um estado explícito. Não transforme exceções em valores silenciosos apenas para preencher o cache.

cached_property ou property?

Use property quando o valor deve refletir imediatamente o estado atual, o cálculo é barato ou a propriedade participa de validação a cada leitura. Use cached_property quando o cálculo é relativamente caro, o resultado permanece estável e a instância pode armazená-lo.

class Circulo:
    def __init__(self, raio):
        self.raio = raio

    @property
    def diametro(self):
        return self.raio * 2

Não há benefício prático em colocar um cálculo tão simples no cache. O custo de memória e invalidação seria maior do que a multiplicação.

cached_property ou lru_cache?

lru_cache armazena resultados por combinação de argumentos de uma função. Ele é excelente para funções puras chamadas repetidamente. Em métodos, pode incluir self na chave, exigindo que a instância seja hashable e mantendo referências aos objetos enquanto as entradas permanecerem no cache.

cached_property é mais direto quando existe um único valor por instância e a chamada não recebe argumentos. O resultado acompanha a vida do objeto e pode ser invalidado com del. Já lru_cache oferece limite de entradas, estatísticas e limpeza global por função.

Testando o cache

Um teste deve confirmar o resultado, a quantidade de execuções e a invalidação:

from functools import cached_property

class Exemplo:
    def __init__(self):
        self.chamadas = 0

    @cached_property
    def valor(self):
        self.chamadas += 1
        return 42

def test_cached_property():
    obj = Exemplo()

    assert obj.valor == 42
    assert obj.valor == 42
    assert obj.chamadas == 1

    del obj.valor

    assert obj.valor == 42
    assert obj.chamadas == 2

Em testes de objetos mutáveis, verifique também se os setters relevantes removem a entrada correta do __dict__.

Boas práticas

  • Use o decorador apenas para cálculos com custo relevante.
  • Prefira resultados estáveis durante a vida da instância.
  • Documente quais mudanças exigem invalidação.
  • Evite efeitos colaterais dentro da propriedade.
  • Proteja operações não idempotentes em cenários concorrentes.
  • Não devolva coleções mutáveis sem considerar alterações externas.
  • Teste primeiro acesso, reutilização, falha e remoção do cache.
  • Verifique a presença de __dict__ ao usar slots ou tipos especiais.

Conclusão

O cached_property no Python é uma ferramenta pequena, mas muito útil para objetos que possuem valores derivados caros e estáveis. No primeiro acesso, o método calcula o resultado; depois, a instância o trata como um atributo comum. Isso deixa a API simples e evita processamento repetido.

O benefício depende de uma política clara de validade. Antes de aplicar o decorador, identifique quais dados alimentam o cálculo, se eles podem mudar, como o cache será invalidado e se acessos concorrentes são possíveis. Quando essas respostas são simples, cached_property oferece uma otimização legível. Quando não são, uma propriedade comum ou uma camada explícita de cache costuma ser mais segura.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Código Python com funções especializadas por tipo
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    singledispatch no Python: polimorfismo simples

    Aprenda singledispatch no Python para criar funções por tipo, reduzir isinstance e organizar polimorfismo com exemplos práticos.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026
    Código Python com descriptors e atributos
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Descriptors em Python: guia prático

    Aprenda descriptors em Python com __get__, __set__, validação, property, armazenamento por instância, testes e boas práticas.

    Ler mais

    Tempo de leitura: 6 minutos
    22/07/2026
    Criando instalador EXE com ícone personalizado em Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como criar um instalador .exe com ícone personalizado no Python

    Se você já desenvolveu algum script útil, provavelmente já se perguntou como criar um instalador .exe com ícone personalizado no

    Ler mais

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

    Como usar herança múltipla no Python sem bugar seu código

    Entender como usar herança múltipla no Python sem bugar seu código é um dos grandes marcos na jornada de qualquer

    Ler mais

    Tempo de leitura: 9 minutos
    21/04/2026
    Uso do super em Python para resolver problemas de herança
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como usar super() no Python e resolver erros de herança

    Entender como usar super() no Python é um divisor de águas para qualquer desenvolvedor que deseja dominar a Programação Orientada

    Ler mais

    Tempo de leitura: 9 minutos
    11/04/2026
    Leitura de arquivos grandes em Python sem travar o sistema
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Como ler arquivos gigantes sem travar o Python

    Lidar com grandes volumes de dados é um desafio comum na rotina de quem trabalha com programação e ciência de

    Ler mais

    Tempo de leitura: 12 minutos
    16/03/2026