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.
Ouvir o resumo desta aula
Um recap de cerca de 2 minutos na voz do Valim, para ouvir no trânsito ou na academia.
Ler a transcrição do resumo
Resumo da aula: Decoradores próprios e context manager.
Os objetivos desta aula. 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.
Veja o essencial, parte por parte.
Um decorador de cache próprio. Um decorador envolve uma função para acrescentar comportamento.
Um decorador de validação. Use um decorador quando o mesmo comportamento envolve várias funções: cache, validação, log de chamada.
Um context manager para medir tempo. Falta uma ferramenta transversal: medir quanto tempo uma operação leva.
Esse foi o resumo do essencial. Para se aprofundar, leia a aula completa e responda os exercícios.
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 ValueErrorUm 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 dentroUm 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.