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

    A vibrant collection of blue sewing threads arranged with hands on a white background.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    queue no Python: coordene threads

    Aprenda queue no Python para coordenar threads com FIFO, prioridade, backpressure, task_done, join, retries e shutdown seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    A male software engineer working on code in a modern office setting.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    struct no Python: trabalhe com binário

    Aprenda struct no Python para empacotar dados binários, controlar endianness, usar buffers e validar protocolos e arquivos externos.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Close-up of a laptop screen with code and a coffee mug, perfect for tech abstract themes.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tarfile no Python: crie TAR seguro

    Aprenda tarfile no Python para criar TAR comprimido, inspecionar membros e extrair com filtros, limites e proteção contra path traversal.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Row of colorful office binders neatly arranged on a shelf, ideal for organization concepts.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    gzip no Python: comprima arquivos .gz

    Aprenda gzip no Python para ler e gravar .gz, criar saídas reproduzíveis, trabalhar com streams e limitar a expansão de

    Ler mais

    Tempo de leitura: 4 minutos
    17/08/2026
    Color-coded office binders organized neatly in a storage shelf, featuring labels and a striking red binder.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    lzma no Python: comprima arquivos XZ

    Aprenda lzma no Python para criar arquivos XZ, usar streams, checks, filtros e limites de memória ao descompactar dados externos.

    Ler mais

    Tempo de leitura: 5 minutos
    17/08/2026
    Exquisite python skin handbag with intricate snake emblem and elegant design.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    bz2 no Python: comprima com bzip2

    Aprenda bz2 no Python para comprimir arquivos e bytes, processar fluxos em blocos e limitar a expansão de dados externos.

    Ler mais

    Tempo de leitura: 5 minutos
    16/08/2026