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 = yNesse 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 * 2Nã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 == 2Em 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.






