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