Entendendo Decorators: O Conceito Fundamental
Um decorator em Python é uma função que recebe outra função como argumento, adiciona alguma funcionalidade a ela sem modificar sua estrutura original, e retorna uma nova função com essa funcionalidade estendida. Pense em um decorator como um "embrulho" que você coloca ao redor de uma função — o conteúdo interno continua o mesmo, mas agora há camadas adicionais que podem fazer algo antes, depois ou até mesmo durante a execução.
A razão pela qual decorators são tão poderosos é que eles respeitam o princípio DRY (Don't Repeat Yourself) e permitem separação de responsabilidades. Em vez de copiar código de validação, logging ou controle de tempo em múltiplas funções, você escreve isso uma única vez em um decorator e o reutiliza. Isso também torna seu código mais legível e fácil de manter.
Vamos começar com um exemplo prático e simples. Imagine que você quer saber quanto tempo cada função leva para executar:
import time
from functools import wraps
def cronometro(func):
@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
@cronometro
def calcular_fibonacci(n):
if n <= 1:
return n
return calcular_fibonacci(n-1) + calcular_fibonacci(n-2)
calcular_fibonacci(10)
Neste exemplo, o decorator cronometro envolve qualquer função e mede seu tempo de execução. O @wraps é importante — ele preserva o nome e a documentação originais da função decorada, evitando que você perca metadados.
Por que usar @wraps?
Sem @wraps, a função decorada perde seu __name__ original e documentação. O decorator @wraps (vindo de functools) copia esses metadados para a função wrapper, mantendo a identidade original intacta. Isso é especialmente útil em debugging e documentação automática.
Criando Seu Primeiro Decorator Funcional
Para dominar decorators, você precisa entender o fluxo de execução em camadas. Um decorator é, fundamentalmente, uma função que retorna outra função. Quando você usa @nome_decorator, Python automaticamente transforma funcao() em decorator(funcao)().
Vamos criar um decorator prático que valida se um parâmetro é um número positivo:
from functools import wraps
def valida_positivo(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Verifica se o primeiro argumento é positivo
if args and not isinstance(args[0], (int, float)):
raise TypeError(f"Primeiro argumento deve ser número, recebeu {type(args[0])}")
if args and args[0] <= 0:
raise ValueError("Primeiro argumento deve ser positivo")
return func(*args, **kwargs)
return wrapper
@valida_positivo
def calcular_raiz_quadrada(numero):
return numero ** 0.5
print(calcular_raiz_quadrada(16)) # Saída: 4.0
print(calcular_raiz_quadrada(-5)) # Levanta ValueError
Este decorator valida a entrada antes de a função ser executada. É uma forma elegante de adicionar guardrails sem poluir a função principal com código de validação. Se você tiver dez funções que precisam dessa validação, aplica o decorator em cada uma — simples e consistente.
Anatomia de um Decorator
O padrão básico sempre segue esta estrutura:
def meu_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Lógica ANTES da função
resultado = func(*args, **kwargs)
# Lógica DEPOIS da função
return resultado
return wrapper
A *args captura argumentos posicionais e **kwargs captura argumentos nomeados. Isso garante que seu decorator funcione com qualquer assinatura de função.
Empilhando Decorators: Composição em Ação
Um dos recursos mais poderosos de Python é a capacidade de aplicar múltiplos decorators em uma única função. Quando você empilha decorators, eles são aplicados de baixo para cima — ou seja, o decorator mais próximo da função executa por último na cadeia, mas sua função wrapper executa primeiro.
Vamos criar dois decorators práticos e vê-los trabalhando juntos:
from functools import wraps
import time
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] Chamando {func.__name__} com args={args}, kwargs={kwargs}")
resultado = func(*args, **kwargs)
print(f"[LOG] {func.__name__} retornou {resultado}")
return resultado
return wrapper
def tempo_execucao(func):
@wraps(func)
def wrapper(*args, **kwargs):
inicio = time.time()
resultado = func(*args, **kwargs)
tempo = time.time() - inicio
print(f"[TEMPO] {func.__name__} executou em {tempo:.6f}s")
return resultado
return wrapper
@logger
@tempo_execucao
def multiplicar(a, b):
time.sleep(0.1)
return a * b
multiplicar(3, 5)
Ordem de execução: Quando você chama multiplicar(3, 5), a execução ocorre assim:
loggerwrapper começa (imprime o log de entrada)tempo_execucaowrapper começa (inicia o cronômetro)- A função original
multiplicarexecuta tempo_execucaowrapper termina (imprime o tempo)loggerwrapper termina (imprime o log de saída)
A ordem importa! Se você inverter os decorators (@tempo_execucao em cima de @logger), o log será envolvido pelo cronômetro, o que mudará o que está sendo medido. Teste ambas as formas e você verá a diferença.
Decorators com Estado Compartilhado
Às vezes, você quer que múltiplos decorators compartilhem informação. Embora raro, é possível:
def criar_contexto_decorador():
contexto = {"chamadas": 0}
def contador(func):
@wraps(func)
def wrapper(*args, **kwargs):
contexto["chamadas"] += 1
print(f"Chamada #{contexto['chamadas']}")
return func(*args, **kwargs)
return wrapper
return contador
contador_global = criar_contexto_decorador()
@contador_global
def saudar(nome):
print(f"Olá, {nome}!")
saudar("Alice") # Chamada #1
saudar("Bob") # Chamada #2
Parametrizando Decorators: Flexibilidade Total
Decorators parametrizados permitem que você customize seu comportamento no momento da aplicação. Em vez de @decorator, você usa @decorator(parametro). Isso adiciona uma camada extra de nesting, mas oferece flexibilidade imensa.
A estrutura de um decorator parametrizado é: uma função que retorna um decorator que retorna um wrapper:
from functools import wraps
def repetir(vezes):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
resultados = []
for i in range(vezes):
resultado = func(*args, **kwargs)
resultados.append(resultado)
return resultados
return wrapper
return decorator
@repetir(vezes=3)
def cumprimento(nome):
return f"Olá, {nome}!"
print(cumprimento("Maria"))
# Saída: ['Olá, Maria!', 'Olá, Maria!', 'Olá, Maria!']
Neste exemplo, @repetir(vezes=3) cria um decorator que executa a função três vezes e retorna uma lista com todos os resultados. A parametrização torna o decorator reutilizável em contextos diferentes — você pode usar @repetir(vezes=5) em outra função.
Exemplo Prático: Decorator com Múltiplos Parâmetros
Vamos criar um decorator mais complexo que combina logging, retry e validação:
from functools import wraps
import time
def resiliente(tentativas=3, aguardar=1, log_erros=True):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
ultima_excecao = None
for tentativa in range(1, tentativas + 1):
try:
return func(*args, **kwargs)
except Exception as e:
ultima_excecao = e
if log_erros:
print(f"[ERRO] Tentativa {tentativa}/{tentativas} falhou: {e}")
if tentativa < tentativas:
print(f"[AGUARDANDO] {aguardar}s antes da próxima tentativa...")
time.sleep(aguardar)
raise ultima_excecao
return wrapper
return decorator
@resiliente(tentativas=3, aguardar=0.5, log_erros=True)
def operacao_instavel(numero):
import random
if random.random() < 0.7:
raise ConnectionError("Falha na conexão")
return f"Sucesso com {numero}"
try:
print(operacao_instavel(42))
except Exception as e:
print(f"Falha final: {e}")
Este decorator é especialmente útil para operações que podem falhar temporariamente (chamadas a APIs, leitura de banco de dados) — ele tenta de novo automaticamente.
Decorators Parametrizados com Valores Padrão
Você pode tornar seus parâmetros opcionais:
from functools import wraps
def debug(prefixo="DEBUG"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{prefixo}] Executando {func.__name__}")
resultado = func(*args, **kwargs)
print(f"[{prefixo}] Resultado: {resultado}")
return resultado
return wrapper
return decorator
@debug() # Usa prefixo padrão
def funcao_um():
return "Um"
@debug(prefixo="INFO") # Customiza o prefixo
def funcao_dois():
return "Dois"
funcao_um()
funcao_dois()
Conclusão
Você aprendeu que decorators são uma ferramenta de composição funcional que permite adicionar comportamento a funções sem modificar seu código original — e que isso respeita o princípio de responsabilidade única. Em segundo lugar, empilhar decorators oferece elegância e modularidade, permitindo que você combine funcionalidades de forma clara e legível, desde que entenda a ordem de execução. Por fim, parametrizar decorators transforma-os em ferramentas altamente reutilizáveis, capazes de se adaptar a contextos diferentes sem duplicação de código.
A verdadeira maestria vem com a prática: escreva decorators para logging, validação, cache e tratamento de erros em seus projetos reais. Você descobrirá que muitos problemas de engenharia de código têm uma solução elegante através de decorators bem projetados.