Estratégias de Caching Avançado com functools.lru_cache em Python

caching é uma técnica para armazenar resultados de funções e reutilizá-los, evitando recomputações custosas. Em Python, o módulo functools oferece o decorator lru_cache, que implementa um cache de tamanho limitado baseado em política LRU (Least Recently Used).

lru_cache é extremamente útil para funções puras que são chamadas repetidamente com os mesmos parâmetros, como funções recursivas, cálculos matemáticos intensivos ou queries simuladas em memória.


1. Conceito de LRU Cache

LRU (Least Recently Used) significa que o item mais antigo e menos acessado é removido do cache quando o limite de armazenamento é atingido.

  • Permite balancear uso de memória e velocidade.

  • Evita que cache cresça indefinidamente em aplicações de longo prazo.


2. Uso Básico do lru_cache

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(35))
  • maxsize define o número máximo de resultados armazenados.

  • Funções puras são ideais: mesmos argumentos → mesmos resultados.


3. Monitorando o Cache

O decorator oferece métodos para inspeção:

print(fibonacci.cache_info())
# CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)
  • hits: chamadas atendidas pelo cache

  • misses: chamadas computadas normalmente

  • currsize: tamanho atual do cache

  • maxsize: limite do cache

Limpeza do cache

fibonacci.cache_clear()  # limpa todo o cache
  • Útil para resetar resultados obsoletos.


4. Estratégias Avançadas

4.1 Ajustando maxsize

  • maxsize=None → cache ilimitado, útil quando se tem memória suficiente e função chamada repetidamente.

  • Pequenos valores → menor uso de memória, mas mais recomputações.

  • Valores grandes → melhor performance, maior consumo de memória.

@lru_cache(maxsize=None)  # cache infinito
def heavy_computation(x):
    return x**2

4.2 Funções com múltiplos argumentos

@lru_cache(maxsize=256)
def process_data(config_tuple):
    return sum(config_tuple)
  • Evita erros de hash e garante cache consistente.

4.3 Composição de caches

@lru_cache(maxsize=128)
def preprocess(x):
    print(f"Preprocessing {x}")
    return x*2

@lru_cache(maxsize=64)
def compute(x):
    return preprocess(x) + 10

print(compute(5))  # Chama preprocess
print(compute(5))  # Usará cache de compute e preprocess

5. Caching em Funções Recursivas Complexas

lru_cache é perfeito para algoritmos recursivos, como Fibonacci, caminhos em grafos e DP (programação dinâmica).

@lru_cache(maxsize=None)
def caminhos(grafo, inicio, fim):
    if inicio == fim:
        return [[inicio]]
    result = []
    for vizinho in grafo.get(inicio, []):
        for caminho in caminhos(grafo, vizinho, fim):
            result.append([inicio] + caminho)
    return result

grafo = {'A':['B','C'], 'B':['C','D'], 'C':['D'], 'D':[]}
print(caminhos(tuple(grafo.items()), 'A', 'D'))
  • Converte grafo em tuple para ser hashable, permitindo caching seguro.


6. Combinação com Paralelização

from concurrent.futures import ThreadPoolExecutor
import time

@lru_cache(maxsize=128)
def slow(x):
    time.sleep(1)
    return x**2

dados = range(5)
with ThreadPoolExecutor() as executor:
    resultados = list(executor.map(slow, dados))
  • Cache evita recomputações mesmo em chamadas concorrentes.


7. Boas Práticas Profissionais

  1. Prefira funções puras, sem efeitos colaterais, para evitar resultados inconsistentes.

  2. Use maxsize adequado ao uso de memória disponível.

  3. Combine caches em pipelines modulares, acelerando cada etapa.

  4. Documente claramente quando e como o cache é usado, principalmente em produção.

  5. Evite caching de dados mutáveis não-hashable.

  6. Use cache_info() para monitorar performance e acertos do cache.


8. Aplicações Profissionais


9. Conclusão

  • functools.lru_cache é uma ferramenta poderosa para melhorar performance de funções repetitivas e pesadas.

  • Estratégias avançadas incluem ajuste de maxsize, composição de caches e caching em pipelines complexos.

  • Quando combinada com funções puras, paralelização e monitoramento, permite otimizar algoritmos críticos em Python sem alterar lógica do código.

Comentários

Postagens mais visitadas deste blog

Laços de Repetição em Python: Conceitos e Exemplos Práticos

Manipulação de Arquivos no C#: Como Ler, Escrever e Trabalhar com Arquivos de Forma Simples

Como Instalar o Xamarin com C#: Passo a Passo Completo