Decoradores em Python: como funcionam e quando usar
1. Fundamentos: O que é um decorador em Python?
Decoradores são um dos recursos mais elegantes e poderosos do Python. Para entendê-los completamente, precisamos primeiro compreender dois conceitos fundamentais: funções de primeira classe e closures.
Em Python, funções são cidadãs de primeira classe — podem ser atribuídas a variáveis, passadas como argumentos e retornadas por outras funções. Um closure ocorre quando uma função interna captura variáveis do escopo da função externa, mesmo após essa função externa ter encerrado sua execução.
Um decorador é essencialmente uma função que recebe outra função como argumento, adiciona algum comportamento a ela e retorna uma nova função. A sintaxe com @ é apenas açúcar sintático:
def meu_decorador(func):
def wrapper():
print("Antes da execução")
func()
print("Depois da execução")
return wrapper
@meu_decorador
def dizer_ola():
print("Olá!")
# Equivalente manual:
# dizer_ola = meu_decorador(dizer_ola)
dizer_ola()
Saída:
Antes da execução
Olá!
Depois da execução
2. Anatomia de um decorador: como implementar do zero
Um decorador simples segue uma estrutura padrão: uma função externa que recebe a função original e uma função interna (wrapper) que adiciona o comportamento extra.
def meu_decorador(func):
def wrapper(*args, **kwargs):
# Código antes da execução
resultado = func(*args, **kwargs)
# Código depois da execução
return resultado
return wrapper
No entanto, essa implementação simples tem um problema: ela perde os metadados da função original, como nome, docstring e assinatura. Para preservar esses metadados, usamos functools.wraps:
import functools
def meu_decorador(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Chamando {func.__name__}")
return func(*args, **kwargs)
return wrapper
@meu_decorador
def soma(a, b):
"""Retorna a soma de a e b."""
return a + b
print(soma.__name__) # 'soma' (sem wraps seria 'wrapper')
print(soma.__doc__) # 'Retorna a soma de a e b.' (sem wraps seria None)
3. Decoradores com argumentos: flexibilidade além do básico
Para criar decoradores que aceitam argumentos, precisamos de três níveis de aninhamento:
import functools
import time
def retry(tentativas=3, delay=1):
def decorador(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(tentativas):
try:
return func(*args, **kwargs)
except Exception as e:
if i == tentativas - 1:
raise
print(f"Tentativa {i+1} falhou: {e}. Tentando novamente...")
time.sleep(delay)
return None
return wrapper
return decorador
@retry(tentativas=5, delay=2)
def conexao_instavel():
import random
if random.random() < 0.7:
raise ConnectionError("Falha na conexão")
return "Conectado!"
print(conexao_instavel())
4. Decoradores aplicados a métodos de classe
Decorar métodos de classe segue o mesmo princípio, mas precisamos lembrar que o primeiro argumento do método é self:
import functools
import time
def medir_tempo(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} segundos")
return resultado
return wrapper
class Calculadora:
@medir_tempo
def calcular_fatorial(self, n):
resultado = 1
for i in range(1, n + 1):
resultado *= i
time.sleep(0.01) # Simula processamento
return resultado
calc = Calculadora()
print(calc.calcular_fatorial(10))
Python também fornece decoradores nativos importantes para classes:
class MinhaClasse:
@staticmethod
def metodo_estatico():
return "Não precisa de instância"
@classmethod
def metodo_classe(cls):
return f"Método da classe {cls.__name__}"
@property
def propriedade(self):
return "Acessado como atributo"
5. Casos de uso reais e boas práticas
Logging automático:
import functools
import logging
logging.basicConfig(level=logging.INFO)
def log_chamada(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"{func.__name__} retornou {resultado}")
return resultado
return wrapper
@log_chamada
def dividir(a, b):
return a / b
dividir(10, 2)
Validação de tipos:
import functools
def validar_tipos(*tipos_args, **tipos_kwargs):
def decorador(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i, (arg, tipo) in enumerate(zip(args, tipos_args)):
if not isinstance(arg, tipo):
raise TypeError(f"Argumento {i} deve ser {tipo.__name__}")
for nome, tipo in tipos_kwargs.items():
if nome in kwargs and not isinstance(kwargs[nome], tipo):
raise TypeError(f"{nome} deve ser {tipo.__name__}")
return func(*args, **kwargs)
return wrapper
return decorador
@validar_tipos(int, int)
def somar(a, b):
return a + b
print(somar(1, 2)) # Funciona
# somar(1, "2") # Levanta TypeError
Controle de acesso em APIs:
import functools
def login_required(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
usuario = kwargs.get('usuario')
if not usuario or not usuario.get('autenticado'):
raise PermissionError("Usuário não autenticado")
return func(*args, **kwargs)
return wrapper
@login_required
def acessar_dados_sensiveis(usuario=None):
return "Dados confidenciais"
# usuario_valido = {'nome': 'João', 'autenticado': True}
# print(acessar_dados_sensiveis(usuario=usuario_valido))
6. Armadilhas comuns e como evitá-las
Mutabilidade de estado compartilhado:
def contador_chamadas(func):
contador = 0
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal contador
contador += 1
print(f"Chamada número {contador}")
return func(*args, **kwargs)
return wrapper
@contador_chamadas
def minha_funcao():
pass
minha_funcao() # Chamada número 1
minha_funcao() # Chamada número 2
Uso de *args, **kwargs para flexibilidade:
def decorador_flexivel(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Recebidos: {len(args)} args posicionais e {len(kwargs)} kwargs")
return func(*args, **kwargs)
return wrapper
Ordem de empilhamento de decoradores:
@decorador_a
@decorador_b
@decorador_c
def funcao():
pass
# Equivalente a:
# funcao = decorador_a(decorador_b(decorador_c(funcao)))
A ordem importa: o decorador mais próximo da função é aplicado primeiro.
7. Alternativas e quando não usar decoradores
Context managers (with) são mais adequados para setup/teardown:
# Decorador (menos idiomático para este caso)
@abrir_arquivo("arquivo.txt")
def processar(arquivo):
return arquivo.read()
# Context manager (mais idiomático)
with open("arquivo.txt") as arquivo:
conteudo = arquivo.read()
Funções de ordem superior sem açúcar sintático podem ser mais explícitas:
# Com decorador
@validar
def processar(dados):
pass
# Sem decorador
def processar(dados):
pass
processar = validar(processar)
Herança ou mixins são mais adequados quando você precisa adicionar comportamento a múltiplos métodos de uma classe:
class LogavelMixin:
def log(self, mensagem):
print(f"[LOG] {mensagem}")
class Servico(LogavelMixin):
def executar(self):
self.log("Executando serviço")
return "Feito"
Referências
- Documentação Oficial do Python sobre Decoradores — Glossário oficial com definição e exemplos básicos de decoradores em Python.
- PEP 318 — Decorators for Functions and Methods — Proposta original que introduziu decoradores na linguagem Python.
- Python Decorators: A Complete Guide (Real Python) — Tutorial abrangente cobrindo desde conceitos básicos até padrões avançados de decoradores.
- functools.wraps Documentation — Documentação oficial da função wraps, essencial para preservar metadados em decoradores.
- Decorators in Python (GeeksforGeeks) — Guia prático com exemplos detalhados e casos de uso comuns de decoradores.
- Python Decorators: How to Use Them (Programiz) — Tutorial passo a passo com exemplos interativos e explicações claras sobre decoradores.