Decorators avançado
Aprofundamento em decorators Python: functools.wraps, decorators de classe, empilhamento e exemplos reais como cache e logging.
Nesta aula, vamos explorar conceitos avançados de decorators em Python. Você aprenderá como preservar metadados de funções decoradas com functools.wraps, como criar decorators baseados em classes, como empilhar múltiplos decorators e ver exemplos práticos usados em projetos reais.
Decorators são uma ferramenta poderosa para modificar ou estender o comportamento de funções ou métodos. Dominar essas técnicas avançadas permite escrever código mais limpo, reutilizável e expressivo.
functools.wraps
Ao criar um decorator, a função decorada perde seus metadados originais, como __name__, __doc__ e __module__. Isso pode causar problemas de introspecção e depuração. O decorator @functools.wraps resolve esse problema copiando esses atributos da função original para a função wrapper.
Exemplo: sem wraps, a função decorada terá o nome do wrapper. Com wraps, preserva-se o nome e a docstring originais.
import functools
def meu_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Antes da função")
resultado = func(*args, **kwargs)
print("Depois da função")
return resultado
return wrapper
@meu_decorator
def saudacao(nome):
"""Retorna uma saudação."""
return f"Olá, {nome}!"
print(saudacao.__name__) # saudacao (sem wraps seria 'wrapper')
print(saudacao.__doc__) # Retorna uma saudação.
Sempre use @functools.wraps em seus decorators para manter a introspecção correta.
Decorators de classe
Decorators podem ser implementados como classes, utilizando o método __call__. Isso permite armazenar estado entre chamadas e é útil para decorators que precisam de configuração ou contagem.
Exemplo: um decorator que conta quantas vezes a função foi chamada.
import functools
class ContadorChamadas:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.contagem = 0
def __call__(self, *args, **kwargs):
self.contagem += 1
print(f"Chamada {self.contagem} de {self.func.__name__}")
return self.func(*args, **kwargs)
@ContadorChamadas
def diga_oi():
print("Oi!")
diga_oi()
diga_oi()
# Saída:
# Chamada 1 de diga_oi
# Oi!
# Chamada 2 de diga_oi
# Oi!
Note que usamos functools.update_wrapper para copiar os metadados da função original para a instância da classe, garantindo que a introspecção funcione.
Decorators de classe também podem aceitar argumentos se implementarmos __init__ com parâmetros extras e retornarmos uma instância que é um callable.
Empilhamento
Você pode aplicar múltiplos decorators a uma mesma função. A ordem de aplicação é de baixo para cima: o decorator mais próximo da função é aplicado primeiro, depois o próximo, e assim por diante. Ao chamar a função, a ordem de execução é de cima para baixo (o decorator mais externo executa primeiro).
Exemplo:
import functools
def decorator_a(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Entrando em A")
resultado = func(*args, **kwargs)
print("Saindo de A")
return resultado
return wrapper
def decorator_b(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Entrando em B")
resultado = func(*args, **kwargs)
print("Saindo de B")
return resultado
return wrapper
@decorator_a
@decorator_b
def mensagem(texto):
print(f"Mensagem: {texto}")
mensagem("Olá")
# Saída:
# Entrando em A
# Entrando em B
# Mensagem: Olá
# Saindo de B
# Saindo de A
O empilhamento é útil para combinar comportamentos como logging, timing, validação, etc.
Exemplos reais
Decorators são amplamente usados em frameworks e bibliotecas. Aqui estão dois exemplos práticos.
Cache (Memoização)
Um decorator que armazena resultados de chamadas anteriores para evitar recálculos.
import functools
def cache(func):
memo = {}
@functools.wraps(func)
def wrapper(*args):
if args in memo:
print("Usando cache")
return memo[args]
resultado = func(*args)
memo[args] = resultado
return resultado
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 55 (cálculo otimizado)
Timer para medir tempo de execução
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
fim = time.time()
print(f"{func.__name__} levou {fim - inicio:.4f} segundos")
return resultado
return wrapper
@timer
def tarefa_pesada():
time.sleep(2)
return "Pronto"
print(tarefa_pesada())
Outros exemplos incluem validação de argumentos, controle de acesso, logging automático, etc.
Boas práticas
- Sempre use
@functools.wrapspara preservar metadados. - Evite decorators que alteram a assinatura da função (argumentos) sem necessidade.
- Documente o comportamento do decorator.
- Prefira decorators de função simples a menos que precise de estado.
Referências
- Documentação oficial functools.wraps
- PEP 318 – Decorators for Functions and Methods
- Real Python: Primer on Python Decorators
- GeeksforGeeks: Decorators in Python
- Stack Overflow: How to make a chain of function decorators?
Exercícios
Crie um decorator de classe chamado
Loggerque registra (print) o nome da função e seus argumentos toda vez que a função for chamada. Usefunctools.update_wrapper.✓ Resposta:import functools class Logger: def __init__(self, func): functools.update_wrapper(self, func) self.func = func def __call__(self, *args, **kwargs): print(f"Chamando {self.func.__name__} com args={args}, kwargs={kwargs}") return self.func(*args, **kwargs) @Logger def soma(a, b): return a + b print(soma(2, 3))Empilhe dois decorators: um que converte o retorno para maiúsculas e outro que adiciona pontos de exclamação no final. Aplique na ordem: primeiro maiúsculas, depois exclamação. Teste com uma função que retorna uma string.
✓ Resposta:import functools def maiusculas(func): @functools.wraps(func) def wrapper(*args, **kwargs): resultado = func(*args, **kwargs) return resultado.upper() return wrapper def exclamacao(func): @functools.wraps(func) def wrapper(*args, **kwargs): resultado = func(*args, **kwargs) return resultado + "!!!" return wrapper @exclamacao @maiusculas def saudacao(nome): return f"Olá, {nome}" print(saudacao("João")) # OLÁ, JOÃO!!!Crie um decorator
repetir(n)que aceita um argumentone repete a chamada da função decoradanvezes, retornando uma lista com os resultados.✓ Resposta:import functools def repetir(n): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): resultados = [] for _ in range(n): resultados.append(func(*args, **kwargs)) return resultados return wrapper return decorator @repetir(3) def diga_oi(): return "Oi" print(diga_oi()) # ['Oi', 'Oi', 'Oi']Usando
functools.wraps, crie um decorator que mede o tempo de execução e exibe o resultado em segundos com 3 casas decimais. Teste com uma função que dorme 0.5 segundos.✓ Resposta:import functools import time def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): inicio = time.time() resultado = func(*args, **kwargs) fim = time.time() print(f"{func.__name__} levou {fim - inicio:.3f} segundos") return resultado return wrapper @timer def esperar(): time.sleep(0.5) return "Feito" print(esperar())Implemente um decorator de classe
ContadorInstanciasque conta quantas instâncias de uma classe foram criadas. Dica: decore o método__init__da classe. (Pode usar um decorator de função normal aplicado ao método.)✓ Resposta:import functools def conta_init(cls): original_init = cls.__init__ @functools.wraps(original_init) def novo_init(self, *args, **kwargs): novo_init.contagem += 1 print(f"Instância {novo_init.contagem} criada") return original_init(self, *args, **kwargs) novo_init.contagem = 0 cls.__init__ = novo_init return cls @conta_init class Pessoa: def __init__(self, nome): self.nome = nome p1 = Pessoa("Ana") p2 = Pessoa("João") print(Pessoa.__init__.contagem) # 2