Decoradores: conceito e implementação

1. O que são Decoradores?

Decoradores são uma das funcionalidades mais elegantes e poderosas do Python. Em essência, um decorador é uma função que recebe outra função como argumento e retorna uma nova função com comportamento estendido ou modificado. Pense neles como "embrulhos" que adicionam funcionalidades extras a funções existentes sem modificar seu código interno.

A sintaxe básica utiliza o símbolo @ como açúcar sintático, tornando a aplicação de decoradores limpa e intuitiva:

@meu_decorador
def minha_funcao():
    pass

2. Funções como Objetos de Primeira Classe

Para entender decoradores, é fundamental compreender que em Python funções são objetos de primeira classe. Isso significa que funções podem ser:

  • Atribuídas a variáveis
  • Passadas como argumentos para outras funções
  • Retornadas de outras funções
def saudacao(nome):
    return f"Olá, {nome}!"

# Atribuindo a uma variável
minha_funcao = saudacao
print(minha_funcao("Maria"))  # Olá, Maria!

# Passando como argumento
def executar_funcao(func, arg):
    return func(arg)

print(executar_funcao(saudacao, "João"))  # Olá, João!

# Retornando de outra função
def criar_saudacao(idioma):
    def saudacao_pt(nome):
        return f"Olá, {nome}!"
    def saudacao_en(nome):
        return f"Hello, {nome}!"
    return saudacao_pt if idioma == "pt" else saudacao_en

3. Construindo um Decorador Manualmente

Vamos construir um decorador timer que mede o tempo de execução de uma função:

import time

def timer(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

# Aplicação manual
def calcular_soma(n):
    return sum(range(n))

calcular_soma = timer(calcular_soma)
print(calcular_soma(1000000))

A função wrapper substitui a função original, adicionando a funcionalidade de medição de tempo antes e depois da execução.

4. Açúcar Sintático com @

O Python oferece uma sintaxe mais elegante usando o símbolo @:

@timer
def calcular_soma(n):
    return sum(range(n))

# Equivalente a: calcular_soma = timer(calcular_soma)

print(calcular_soma(1000000))

Vamos criar outro exemplo prático, um decorador @logger:

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Chamando {func.__name__} com args={args}, kwargs={kwargs}")
        resultado = func(*args, **kwargs)
        print(f"{func.__name__} retornou {resultado}")
        return resultado
    return wrapper

@logger
def dividir(a, b):
    return a / b

dividir(10, 2)
# Saída:
# Chamando dividir com args=(10, 2), kwargs={}
# dividir retornou 5.0

5. Decoradores com Argumentos

Para criar decoradores que aceitam parâmetros, precisamos de uma camada extra de aninhamento:

def repetir(vezes):
    def decorador(func):
        def wrapper(*args, **kwargs):
            for _ in range(vezes):
                resultado = func(*args, **kwargs)
            return resultado
        return wrapper
    return decorador

@repetir(3)
def saudacao(nome):
    print(f"Olá, {nome}!")

saudacao("Ana")
# Saída:
# Olá, Ana!
# Olá, Ana!
# Olá, Ana!

A estrutura é: função externa → função interna → wrapper. A função externa recebe os argumentos do decorador e retorna a função decoradora propriamente dita.

6. Preservando Metadados com functools.wraps

Um problema comum ao usar decoradores é a perda dos metadados da função original (nome, docstring, assinatura):

def meu_decorador(func):
    def wrapper(*args, **kwargs):
        """Wrapper interno"""
        return func(*args, **kwargs)
    return wrapper

@meu_decorador
def minha_funcao():
    """Documentação importante"""
    pass

print(minha_funcao.__name__)  # wrapper (perdeu o nome original)
print(minha_funcao.__doc__)   # Wrapper interno (perdeu a docstring)

A solução é usar @functools.wraps:

import functools

def meu_decorador(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper interno"""
        return func(*args, **kwargs)
    return wrapper

@meu_decorador
def minha_funcao():
    """Documentação importante"""
    pass

print(minha_funcao.__name__)  # minha_funcao
print(minha_funcao.__doc__)   # Documentação importante

7. Empilhamento de Decoradores

Podemos aplicar múltiplos decoradores a uma mesma função. A ordem de execução é de baixo para cima (do mais próximo da função para o mais externo):

@timer
@logger
def processar_dados(n):
    return sum(range(n))

# Equivalente a: processar_dados = timer(logger(processar_dados))

processar_dados(100000)
# Ordem de execução:
# 1. timer é o decorador mais externo
# 2. logger é aplicado primeiro à função original
# 3. timer é aplicado ao resultado de logger(processar_dados)

8. Casos de Uso Comuns e Boas Práticas

Controle de acesso e autenticaçã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

Cache de resultados (memoização):

def memoizar(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoizar
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

Validação de argumentos:

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 negativo não permitido: {arg}")
        return func(*args, **kwargs)
    return wrapper

Boas práticas:
- Use decoradores para separar preocupações transversais (logging, timing, validação)
- Sempre use @functools.wraps para preservar metadados
- Evite decoradores excessivamente complexos que prejudiquem a legibilidade
- Documente claramente o comportamento dos decoradores
- Considere criar classes de decoradores para casos mais complexos

Decoradores são ferramentas poderosas que, quando usados com moderação e clareza, podem tornar seu código Python mais elegante, reutilizável e fácil de manter.

Referências