Cache evita repetir operações caras, mas introduz dados potencialmente desatualizados. cachetools oferece políticas com limite de tamanho, incluindo TTLCache, que combina expiração temporal e descarte dos itens menos usados.
Instalar e definir o contrato
python -m pip install cachetools
Antes do código, defina a operação cara, a defasagem aceitável, o orçamento de memória e o evento de invalidação. O cache só é seguro quando o chamador tolera um valor antigo durante o TTL. Autorização, saldo e reserva de estoque podem exigir invalidação explícita ou nenhum cache.
Criar um cache limitado
from cachetools import TTLCache, cached
from threading import RLock
cache = TTLCache(maxsize=500, ttl=60)
lock = RLock()
@cached(cache=cache, lock=lock)
def buscar_produto(produto_id: int) -> dict[str, object]:
return consultar_banco(produto_id)
maxsize limita a quantidade conforme a função de tamanho configurada. ttl define por quanto tempo um valor permanece acessível. O lock protege o acesso ao compartilhar a função entre threads, mas não distribui o cache entre processos.
Por padrão, maxsize conta entradas, não bytes. Quinhentos documentos grandes diferem de quinhentos inteiros. Passe getsizeof se tamanho representar melhor a capacidade, lembrando que ele é calculado na inserção. Alterar o objeto depois torna a contagem imprecisa. Valores imutáveis ou cópias defensivas evitam mudanças silenciosas no estado compartilhado.
Projetar chave e invalidação
Inclua na chave tudo que altera o resultado, como usuário, idioma ou versão de regra. Nunca misture respostas privadas entre usuários. Não armazene exceções transitórias indefinidamente e evite cache para autorização sem uma estratégia de invalidação imediata.
Normalize entradas equivalentes e inclua tenant, permissões e formato quando mudarem a resposta. Mantenha chaves hashable e pequenas. Não use tokens, senhas nem dados pessoais brutos, pois diagnósticos podem expô-los.
Métodos exigem atenção porque self normalmente entra na chave. cachedmethod pode obter um cache de cada instância:
from cachetools import TTLCache, cachedmethod
from cachetools.keys import hashkey
from operator import attrgetter
class Catalogo:
def __init__(self) -> None:
self.cache = TTLCache(maxsize=200, ttl=120)
@cachedmethod(
attrgetter("cache"),
key=lambda self, produto_id, idioma: hashkey(produto_id, idioma.lower()),
)
def carregar(self, produto_id: int, idioma: str) -> dict[str, object]:
return consultar_catalogo(produto_id, idioma)
Entender expiração e remoção
O TTL usa relógio monotônico por padrão, evitando problemas quando o relógio civil muda. Entradas vencidas não são retornadas, mas o espaço pode ser recuperado apenas numa escrita posterior ou com expire(). Em processo com poucas escritas, uma manutenção deliberada facilita controlar a memória.
Ao atingir maxsize, TTLCache remove vencidos primeiro e depois segue LRU. O TTL não garante permanência pelo prazo completo, pois pressão de capacidade pode remover antes. Expiração não é limpeza ativa em background. O código precisa funcionar em todo miss.
removidos = cache.expire()
for chave, valor in removidos:
liberar_recurso(valor)
Não dependa dessa remoção para ação essencial. Reinício ou término abrupto pode ignorá-la.
Controlar concorrência e avalanche
O lock protege o acesso ao cache, mas a função decorada pode executar fora da seção crítica. Algumas versões aceitam uma condição para chamadas concorrentes da mesma chave aguardarem um cálculo. Consulte a documentação da versão instalada e teste o comportamento.
Para origem lenta, use também timeout e concorrência limitada. TTL curto e tráfego sincronizado podem gerar muitos misses juntos. Considere jitter limitado, atualização antecipada de valores populares ou cache externo com coordenação distribuída. Não sirva dados antigos indefinidamente para esconder uma indisponibilidade.
Invalidar deliberadamente
Tempo é só uma estratégia. Após atualizar um produto, remova sua chave ou incremente uma versão incluída nela. Namespaces versionados ajudam quando muitas entradas dependem da mesma regra. cache.clear() é simples, mas pode provocar pico súbito no banco.
Cache negativo pode guardar “não encontrado” por pouco tempo. Use TTL menor e diferencie ausência de falha transitória. Timeout, permissão negada e registro ausente precisam de tratamentos distintos.
Testar sem esperar
class Relogio:
agora = 0.0
def __call__(self) -> float:
return self.agora
relogio = Relogio()
cache_teste = TTLCache(maxsize=2, ttl=10, timer=relogio)
cache_teste["a"] = 1
relogio.agora = 11
assert "a" not in cache_teste
Teste acertos, misses, expiração, capacidade, invalidação, isolamento e concorrência. Meça taxa de acerto junto com latência e erros da origem. Taxa alta não ajuda se os valores estão velhos; taxa baixa pode indicar que a complexidade não compensa.
Um cache em memória some no reinício e diverge entre workers. Se várias instâncias precisam da mesma visão, considere um serviço externo e aceite a complexidade operacional. Meça acertos, perdas, latência e tamanho; o artigo sobre métricas com Prometheus Client mostra como observar o comportamento.
Cache local serve para resultados descartáveis num processo. Escolha cache externo quando workers precisam de invalidação compartilhada, limites coordenados ou valores além da implantação. Mesmo assim, preserve banco ou API oficial como fonte da verdade e planeje a perda completa do cache.
Revisar modos de falha
Simule cache vazio na inicialização, cache cheio sob carga, origem indisponível e duas requisições para a mesma chave fria. Confirme que um miss nunca altera a correção e que a memória permanece limitada. Se os valores possuem arquivos, sockets ou outros recursos, gerencie-os fora do cache em vez de presumir que a remoção sempre os fechará.
Documente a unidade do TTL junto à configuração e evite números mágicos. Uma escolha útil considera frequência de mudança da origem, custo do miss e defasagem tolerada. Revise-a quando tráfego ou regras mudarem.
Não exponha o objeto mutável como dicionário geral da aplicação. Encapsule-o numa interface pequena que seja dona das chaves, invalidação e métricas. Assim, chamadores não criam chaves incompatíveis nem ignoram sincronização, e trocar a implementação fica mais simples.
Revise também a mutabilidade quando o valor atravessa uma API. Entregar o mesmo dicionário a vários chamadores pode acoplar requisições sem intenção. Converta objetos de domínio para uma representação estável na fronteira e armazene somente a camada cuja responsabilidade está clara. Documente se o retorno pode ser alterado e use cópia quando necessário. Essa disciplina reduz bugs que parecem falhas de expiração, mas são modificações locais invisíveis.
A documentação oficial do cachetools, consultada em 22 de julho de 2026, descreve LRU, TTL, funções de chave e sincronização. Cache é uma otimização descartável; o banco ou serviço de origem continua sendo a fonte de verdade.