Decorators avançado
Nesta aula avançada sobre decorators em Python, exploramos o uso de functools.wraps para preservar metadados, decorators de classe, empilhamento de decorators e exemplos reais como logging, timing e autenticação. O conteúdo inclui explicações detalhadas, exemplos práticos de código e exercícios com respostas.
Decorators são uma ferramenta poderosa em Python que permitem modificar ou estender o comportamento de funções e métodos sem alterar seu código fonte. Nesta aula avançada, vamos aprofundar em técnicas como o uso de functools.wraps para preservar metadados, criar decorators baseados em classes, empilhar múltiplos decorators e aplicar esses conceitos em exemplos reais do dia a dia.
Dominar esses tópicos é essencial para escrever código Python elegante, reutilizável e profissional. Vamos começar!
functools.wraps
Quando escrevemos um decorator, a função decorada perde seus metadados originais, como nome, docstring e módulo. Isso pode causar confusão durante depuração e inspeção. O decorator @functools.wraps resolve esse problema copiando os atributos da função original para a função wrapper.
Exemplo sem wraps:
def meu_decorator(func):
def wrapper(*args, **kwargs):
print("Antes")
resultado = func(*args, **kwargs)
print("Depois")
return resultado
return wrapper
@meu_decorator
def saudacao(nome):
"""Retorna uma saudação."""
return f"Olá, {nome}!"
print(saudacao.__name__) # 'wrapper'
print(saudacao.__doc__) # None
Com wraps, os metadados são preservados:
import functools
def meu_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Antes")
resultado = func(*args, **kwargs)
print("Depois")
return resultado
return wrapper
@meu_decorator
def saudacao(nome):
"""Retorna uma saudação."""
return f"Olá, {nome}!"
print(saudacao.__name__) # 'saudacao'
print(saudacao.__doc__) # 'Retorna uma saudação.'
Além de __name__ e __doc__, wraps também copia __module__, __annotations__ e __dict__. É uma boa prática sempre usar @functools.wraps em seus decorators.
Decorators de classe
Decorators não precisam ser funções; também podem ser classes. Uma classe decorator deve implementar o método __call__ para que sua instância seja chamável. Isso é útil quando precisamos manter estado ou configurar o decorator com parâmetros.
Exemplo de decorator de classe que conta chamadas:
class ContadorChamadas:
def __init__(self, func):
self.func = func
self.contador = 0
def __call__(self, *args, **kwargs):
self.contador += 1
print(f"Chamada {self.contador} de {self.func.__name__}")
return self.func(*args, **kwargs)
@ContadorChamadas
def diga_oi():
print("Oi!")
diga_oi() # Chamada 1 de diga_oi
diga_oi() # Chamada 2 de diga_oi
Decorators de classe também podem aceitar argumentos. Para isso, a classe deve ser instanciada com os argumentos e depois o objeto é usado como decorator (chamando __call__ com a função). Exemplo:
class Repetir:
def __init__(self, vezes):
self.vezes = vezes
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(self.vezes):
func(*args, **kwargs)
return wrapper
@Repetir(3)
def diga_oi():
print("Oi!")
diga_oi() # Oi! Oi! Oi!
Empilhamento
Você pode aplicar múltiplos decorators a uma única função. Eles são aplicados de baixo para cima (da função para cima). A ordem importa e pode afetar o comportamento final.
Exemplo:
@decorator1
@decorator2
def funcao():
pass
É equivalente a funcao = decorator1(decorator2(funcao)). Vamos ver um exemplo concreto com dois decorators simples:
def maiusculas(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
resultado = func(*args, **kwargs)
return resultado.upper()
return wrapper
def enfatizar(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
resultado = func(*args, **kwargs)
return resultado + "!!!"
return wrapper
@maiusculas
@enfatizar
def saudacao(nome):
return f"Olá, {nome}"
print(saudacao("João")) # OLÁ, JOÃO!!!
Note que @enfatizar é aplicado primeiro, depois @maiusculas. Se invertêssemos a ordem, o resultado seria "OLÁ, JOÃO!!!" mas com exclamações dentro da string. A ordem deve ser escolhida com cuidado.
Exemplos reais
Vamos ver alguns decorators úteis em projetos reais.
Logging: Registra chamadas de função com argumentos e resultados.
import logging
logging.basicConfig(level=logging.INFO)
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Chamando {func.__name__} com args={args}, kwargs={kwargs}")
resultado = func(*args, **kwargs)
logging.info(f"Resultado: {resultado}")
return resultado
return wrapper
@log
def soma(a, b):
return a + b
soma(2, 3) # INFO:root:Chamando soma com args=(2, 3), kwargs={} ... Resultado: 5
Timing: Mede o tempo de execução de uma função.
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 dorme():
time.sleep(1)
dorme() # dorme levou 1.0001 segundos
Autenticação: Simula verificação de usuário antes de executar uma função.
def requer_autenticacao(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not usuario_autenticado():
raise PermissionError("Usuário não autenticado")
return func(*args, **kwargs)
return wrapper
def usuario_autenticado():
# Simulação
return True
@requer_autenticacao
def dados_sensiveis():
return "Dados secretos"
print(dados_sensiveis())
Cache: Armazena resultados de funções caras.
def cache(func):
memoria = {}
@functools.wraps(func)
def wrapper(*args):
if args in memoria:
print("Usando cache")
return memoria[args]
resultado = func(*args)
memoria[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)
print(fibonacci(10)) # 55 (cache)
Boas práticas
Sempre use @functools.wraps para preservar metadados. Prefira decorators baseados em funções quando não precisar de estado; use classes quando precisar de estado ou configuração complexa. Documente seus decorators com docstrings claras. Lembre-se de que decorators podem ser aplicados a métodos de classe também, mas cuidado com o argumento self.
Referências
- Documentação oficial functools.wraps
- Glossário Python: Decorator
- Real Python: Primer on Python Decorators
- GeeksforGeeks: Decorators in Python
- Python Functional Programming HOWTO: Decorators
Exercícios
Crie um decorator chamado
executa_tres_vezesque executa a função decorada três vezes seguidas (sem argumentos). Usefunctools.wraps.✓ Resposta:import functools def executa_tres_vezes(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(3): func(*args, **kwargs) return wrapperImplemente um decorator de classe
Contadorque conta quantas vezes a função decorada foi chamada. A classe deve armazenar o contador em um atributocontagem.✓ Resposta:import functools class Contador: def __init__(self, func): self.func = func self.contagem = 0 def __call__(self, *args, **kwargs): self.contagem += 1 return self.func(*args, **kwargs)Empilhe os decorators
@maiusculase@enfatizar(definidos na aula) em uma funçãomensagem(texto)que retorna o texto. Teste ambas as ordens.✓ Resposta:import functools def maiusculas(func): @functools.wraps(func) def wrapper(*args, **kwargs): resultado = func(*args, **kwargs) return resultado.upper() return wrapper def enfatizar(func): @functools.wraps(func) def wrapper(*args, **kwargs): resultado = func(*args, **kwargs) return resultado + "!!!" return wrapper @maiusculas @enfatizar def mensagem(texto): return texto print(mensagem("olá")) # OLÁ!!! # Invertendo a ordem: @enfatizar @maiusculas def mensagem2(texto): return texto print(mensagem2("olá")) # OLÁ!!!Crie um decorator
validar_positivoque verifica se todos os argumentos numéricos são positivos (maiores que zero). Se algum não for, levantaValueError. Usefunctools.wraps.✓ Resposta:import functools def validar_positivo(func): @functools.wraps(func) def wrapper(*args, **kwargs): for arg in args: if isinstance(arg, (int, float)) and arg <= 0: raise ValueError(f"Argumento {arg} não é positivo") for key, value in kwargs.items(): if isinstance(value, (int, float)) and value <= 0: raise ValueError(f"Argumento {key}={value} não é positivo") return func(*args, **kwargs) return wrapperEscreva um decorator
loggerque registra em um arquivolog.txto nome da função e o timestamp de cada chamada. Use o módulodatetime.✓ Resposta:import functools from datetime import datetime def logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): with open("log.txt", "a") as f: f.write(f"{datetime.now()} - {func.__name__} chamada\n") return func(*args, **kwargs) return wrapper