Decoradores com parâmetros e empilhamento
1. Revisão rápida: Decoradores simples
Decoradores são um dos recursos mais elegantes do Python. Um decorador é essencialmente uma função que recebe outra função como argumento e retorna uma nova função, geralmente estendendo seu comportamento. A sintaxe @decorador é açúcar sintático para funcao = decorador(funcao).
Vamos começar com um exemplo clássico: um decorador @timer que mede o tempo de execução de uma função:
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
fim = time.time()
print(f"{func.__name__} executou em {fim - inicio:.4f}s")
return resultado
return wrapper
@timer
def calcular_soma(n):
return sum(range(n))
calcular_soma(1000000) # calcular_soma executou em 0.0472s
2. Decoradores com parâmetros: conceito e motivação
Decoradores simples são ótimos, mas frequentemente precisamos de configuração dinâmica. Por exemplo, um decorador @retry que deve aceitar o número de tentativas e o delay entre elas. Para isso, precisamos de uma estrutura de três níveis:
- Função de fábrica (recebe os parâmetros do decorador)
- Decorador intermediário (recebe a função original)
- Wrapper interno (recebe os argumentos da função original)
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for tentativa in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if tentativa == max_attempts - 1:
raise
print(f"Tentativa {tentativa + 1} falhou: {e}. Tentando novamente em {delay}s...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def conectar_servidor():
# Simula uma conexão que falha nas primeiras tentativas
raise ConnectionError("Servidor indisponível")
3. Implementação passo a passo de um decorador com parâmetros
Vamos construir um decorador @log que aceita um nível de log e formatação personalizada:
import logging
from datetime import datetime
def log(level="INFO", format_string="{timestamp} [{level}] {func_name}: {message}"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
timestamp = datetime.now().isoformat()
try:
resultado = func(*args, **kwargs)
mensagem = f"Executou com sucesso. Resultado: {resultado}"
log_message = format_string.format(
timestamp=timestamp,
level=level,
func_name=func.__name__,
message=mensagem
)
print(log_message)
return resultado
except Exception as e:
mensagem = f"Falhou com erro: {e}"
log_message = format_string.format(
timestamp=timestamp,
level="ERROR",
func_name=func.__name__,
message=mensagem
)
print(log_message)
raise
return wrapper
return decorator
@log(level="DEBUG", format_string="[{level}] {func_name}: {message}")
def dividir(a, b):
return a / b
dividir(10, 2) # [DEBUG] dividir: Executou com sucesso. Resultado: 5.0
dividir(10, 0) # [ERROR] dividir: Falhou com erro: division by zero
4. Decoradores com parâmetros opcionais
Às vezes queremos que um decorador funcione tanto com parênteses vazios quanto sem parênteses. A técnica envolve detectar se o primeiro argumento é uma função:
def validate(schema=None):
# Se schema é uma função, significa que foi usado sem parênteses: @validate
if callable(schema):
func = schema
schema = None
return validate_with_schema(func, schema)
# Caso contrário, foi usado com parênteses: @validate(schema=...)
return lambda func: validate_with_schema(func, schema)
def validate_with_schema(func, schema):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if schema:
for arg in args:
if not isinstance(arg, schema):
raise TypeError(f"Argumento {arg} não é do tipo {schema.__name__}")
return func(*args, **kwargs)
return wrapper
@validate # Sem parênteses
def processar_nome(nome):
return f"Olá, {nome}"
@validate(schema=str) # Com parênteses
def processar_email(email):
return f"Email: {email}"
print(processar_nome("João")) # Funciona
processar_email(123) # TypeError: Argumento 123 não é do tipo str
5. Empilhamento de decoradores: ordem e execução
Quando empilhamos decoradores, a ordem de aplicação é de baixo para cima, mas a ordem de execução é de cima para baixo. Vamos analisar:
def auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("[AUTH] Verificando autenticação...")
return func(*args, **kwargs)
return wrapper
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] Executando {func.__name__}")
return func(*args, **kwargs)
return wrapper
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
print(f"[TIMER] {func.__name__} levou {time.time() - inicio:.2f}s")
return resultado
return wrapper
@auth
@log
@timer
def acessar_dados(usuario):
print(f"[DADOS] Acessando dados para {usuario}")
time.sleep(0.5)
return {"usuario": usuario, "dados": "confidenciais"}
acessar_dados("admin")
# Saída:
# [AUTH] Verificando autenticação...
# [LOG] Executando acessar_dados
# [TIMER] acessar_dados levou 0.50s
# [DADOS] Acessando dados para admin
6. Combinação de decoradores com parâmetros e empilhamento
Vamos criar um exemplo mais complexo combinando decoradores com parâmetros:
def cache(ttl=60):
cache_store = {}
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(kwargs.items()))
if key in cache_store:
timestamp, resultado = cache_store[key]
if time.time() - timestamp < ttl:
print(f"[CACHE] Retornando valor em cache para {func.__name__}")
return resultado
resultado = func(*args, **kwargs)
cache_store[key] = (time.time(), resultado)
return resultado
return wrapper
return decorator
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for tentativa in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if tentativa == max_attempts - 1:
raise
print(f"[RETRY] Tentativa {tentativa + 1} falhou")
time.sleep(delay)
return wrapper
return decorator
@cache(ttl=30)
@retry(max_attempts=3, delay=2)
def buscar_api(endpoint):
print(f"[API] Buscando {endpoint}...")
# Simula uma API instável
if time.time() % 3 < 1:
raise ConnectionError("Timeout")
return {"status": "ok", "dados": endpoint}
# Primeira chamada: tenta até 3 vezes, depois cacheia
resultado1 = buscar_api("/users")
# Segunda chamada: usa cache
resultado2 = buscar_api("/users")
7. Casos de uso avançados e armadilhas comuns
Decoradores em métodos de classe
def log_method(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
print(f"[LOG] {self.__class__.__name__}.{func.__name__} chamado")
return func(self, *args, **kwargs)
return wrapper
class Servico:
@log_method
def processar(self, dado):
return f"Processado: {dado}"
@classmethod
@log_method
def metodo_classe(cls):
return "Método de classe executado"
servico = Servico()
servico.processar("teste")
Servico.metodo_classe()
Debugging de decoradores empilhados
Use functools.wraps sempre para preservar metadados. Para depuração, você pode usar:
def debug_decorators(func):
"""Decorator para inspecionar a pilha de decoradores"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[DEBUG] Executando: {func.__name__}")
print(f"[DEBUG] Módulo: {func.__module__}")
print(f"[DEBUG] Docstring: {func.__doc__}")
return func(*args, **kwargs)
return wrapper
8. Boas práticas e conclusão
Decoradores com parâmetros e empilhamento são ferramentas poderosas, mas exigem cuidado:
- Sempre use
functools.wrapspara preservar metadados da função original - Mantenha decoradores puros - evite efeitos colaterais inesperados
- Documente claramente o comportamento esperado de cada decorador
- Evite empilhamento excessivo - mais de 3-4 decoradores podem comprometer a legibilidade
- Considere a ordem - lembre-se que a execução começa pelo decorador mais externo
Decoradores bem projetados podem transformar seu código, adicionando funcionalidades como logging, cache, autenticação e tratamento de erros de forma limpa e reutilizável. A chave está em entender profundamente o fluxo de execução e manter a simplicidade sempre que possível.
Referências
- Python Decorators: A Complete Guide — Tutorial abrangente sobre decoradores em Python, incluindo parâmetros e empilhamento
- PEP 318 – Decorators for Functions and Methods — Proposta original que introduziu decoradores na linguagem Python
- Python Documentation: functools.wraps — Documentação oficial sobre a função
wrapspara preservar metadados - Stacking and Chaining Decorators in Python — Guia prático sobre empilhamento de decoradores com exemplos
- Advanced Python Decorators: Parameters and Nesting — Tutorial detalhado sobre decoradores com parâmetros e técnicas avançadas
- Python Decorators with Arguments — Seção do Python Tips sobre decoradores parametrizados e boas práticas
- Understanding Python Decorators in Depth — Curso interativo da DataCamp sobre decoradores, incluindo casos de uso avançados