Módulo 16 - Projeto final: mini biblioteca de utilidades

Decoradores próprios e context manager

11 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Escrever um decorador de cache com functools.wraps.
  • Escrever um decorador de validação de argumentos.
  • Criar um context manager com contextlib para medir tempo.
  • Aplicar essas ferramentas na lógica de preço do catalogo.

Um decorador de cache próprio

Embora a biblioteca padrão já ofereça o lru_cache, escrever um cache próprio é o melhor exercício de decorador que existe, e mostra que você entende o mecanismo por dentro. O decorador recebe uma função, cria um dicionário para guardar resultados, e devolve uma função que consulta esse dicionário antes de calcular. Se o argumento já foi visto, devolve o valor guardado; senão, calcula, guarda e devolve. O detalhe profissional é o functools.wraps, que preserva o nome e a documentação da função original, para que ela não perca a identidade ao ser decorada.

# src/catalogo/infra.py
import functools

def cache_simples(func):
    memoria = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args in memoria:
            return memoria[args]
        resultado = func(*args)
        memoria[args] = resultado
        return resultado

    wrapper.limpar = memoria.clear   # utilitario extra: limpar o cache
    return wrapper

@cache_simples
def fatorial(n):
    return 1 if n <= 1 else n * fatorial(n - 1)

print(fatorial(10))       # calcula e guarda
print(fatorial.__name__)   # fatorial (preservado pelo wraps)

Um decorador de cache próprio, com functools.wraps preservando a identidade da função.

Sem o functools.wraps, a função decorada passaria a se chamar wrapper e perderia sua docstring, o que atrapalha a depuração e a documentação automática. Com ele, fatorial continua se chamando fatorial. Repare também no toque extra, wrapper.limpar, que anexa um utilitário ao próprio wrapper para esvaziar o cache quando preciso. Esse tipo de detalhe mostra a diferença entre copiar um padrão e entendê-lo: você adapta o decorador às suas necessidades, porque sabe o que cada parte faz.

Um decorador de validação

O segundo decorador resolve uma repetição comum: várias funções da lógica de preço recebem um percentual e precisam garantir que ele está entre zero e cem. Em vez de repetir a mesma verificação em cada função, escrevemos um decorador que valida o argumento antes de deixar a função rodar. Um decorador com parâmetro tem uma camada a mais que um decorador simples, porque ele mesmo recebe uma configuração, aqui os limites permitidos. É um passo além, e é ótimo tê-lo no repertório.

# src/catalogo/infra.py (continuacao)
import functools

def validar_faixa(minimo, maximo):
    def decorador(func):
        @functools.wraps(func)
        def wrapper(valor, *args, **kwargs):
            if not (minimo <= valor <= maximo):
                raise ValueError(f"{valor} fora da faixa [{minimo}, {maximo}]")
            return func(valor, *args, **kwargs)
        return wrapper
    return decorador

@validar_faixa(0, 100)
def aplicar_percentual(percentual, base):
    return base * (1 - percentual / 100)

print(aplicar_percentual(10, 200))   # 180.0
# aplicar_percentual(150, 200) levantaria ValueError

Um decorador com parâmetro: validar_faixa recebe limites e checa o argumento antes de executar.

Um context manager para medir tempo

Falta uma ferramenta transversal: medir quanto tempo uma operação leva. O jeito elegante é um context manager, usado com a instrução with. O bloco with garante que a ação de saída aconteça mesmo se houver erro dentro, o que é perfeito para parar o cronômetro. A forma mais direta de criar um context manager próprio é com o decorador contextmanager do módulo contextlib: você escreve uma função geradora, coloca o que roda na entrada antes do yield e o que roda na saída depois dele. É um uso lindo do yield que você já domina.

# src/catalogo/infra.py (continuacao)
import time
from contextlib import contextmanager

@contextmanager
def cronometrar(rotulo):
    inicio = time.perf_counter()
    try:
        yield          # aqui roda o bloco dentro do with
    finally:
        fim = time.perf_counter()
        print(f"{rotulo}: {fim - inicio:.4f}s")

with cronometrar("processar catalogo"):
    total = sum(i * i for i in range(1_000_000))
# ao sair do with, imprime o tempo, mesmo se der erro dentro

Um context manager com contextlib: o que vem antes do yield roda na entrada; o finally, na saída.

O uso do try e finally em torno do yield é o que garante robustez: aconteça o que acontecer no bloco with, o tempo é medido e impresso na saída. Agora a infraestrutura da biblioteca está completa. Temos um cache para não repetir cálculos, um validador para proteger as entradas e um cronômetro para medir. Essas três ferramentas foram escritas uma vez, no módulo infra, e vão ser reutilizadas pela lógica de preço sem repetição. É o princípio DRY e a separação de responsabilidades funcionando juntos: a infraestrutura mora num lugar, o domínio noutro.

Teste rápido

No context manager criado com contextmanager, o que roda depois do yield?

Perguntas frequentes

Por que escrever um cache próprio se existe o lru_cache?
Em produção, o lru_cache costuma ser a escolha. Escrever um cache próprio aqui é exercício: mostra que você entende como um decorador guarda estado e envolve a função. Entender o mecanismo permite adaptar decoradores às suas necessidades, em vez de só aplicar receitas prontas.
O que acontece se eu esquecer o functools.wraps?
A função decorada passa a ter o nome e a docstring da função interna, geralmente wrapper, perdendo a própria identidade. Isso confunde a depuração, quebra a documentação automática e atrapalha ferramentas que inspecionam funções. O wraps preserva esses metadados; use-o sempre em decoradores.
Por que um decorador com parâmetro tem uma função a mais?
Porque ele precisa primeiro receber a configuração, como os limites da faixa, e só depois receber a função a decorar. Isso gera três níveis: a função de fora recebe o parâmetro, a do meio recebe a função, e a de dentro é o wrapper que executa. Cada nível tem um papel claro.
Qual a vantagem de um context manager sobre try e finally solto?
O context manager empacota o padrão de entrada e saída num nome reutilizável, usado com um with limpo. Em vez de repetir try e finally em cada lugar, você escreve a lógica uma vez e a reutiliza. O resultado é mais legível e menos sujeito a esquecer a parte da saída.
contextmanager exige entender geradores?
Sim, e é por isso que ele aparece no avançado. A função usa yield para marcar o ponto em que o bloco with roda: o que vem antes é a entrada, o que vem depois é a saída. Como você já domina geradores, criar context managers assim fica natural e elegante.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.