Nesta aula, vamos explorar conceitos avançados de decorators em Python. Você aprenderá como preservar metadados de funções decoradas com functools.wraps, como criar decorators baseados em classes, como empilhar múltiplos decorators e ver exemplos práticos usados em projetos reais.

Decorators são uma ferramenta poderosa para modificar ou estender o comportamento de funções ou métodos. Dominar essas técnicas avançadas permite escrever código mais limpo, reutilizável e expressivo.

functools.wraps

Ao criar um decorator, a função decorada perde seus metadados originais, como __name__, __doc__ e __module__. Isso pode causar problemas de introspecção e depuração. O decorator @functools.wraps resolve esse problema copiando esses atributos da função original para a função wrapper.

Exemplo: sem wraps, a função decorada terá o nome do wrapper. Com wraps, preserva-se o nome e a docstring originais.

import functools

def meu_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Antes da função")
        resultado = func(*args, **kwargs)
        print("Depois da função")
        return resultado
    return wrapper

@meu_decorator
def saudacao(nome):
    """Retorna uma saudação."""
    return f"Olá, {nome}!"

print(saudacao.__name__)  # saudacao (sem wraps seria 'wrapper')
print(saudacao.__doc__)   # Retorna uma saudação.

Sempre use @functools.wraps em seus decorators para manter a introspecção correta.

Decorators de classe

Decorators podem ser implementados como classes, utilizando o método __call__. Isso permite armazenar estado entre chamadas e é útil para decorators que precisam de configuração ou contagem.

Exemplo: um decorator que conta quantas vezes a função foi chamada.

import functools

class ContadorChamadas:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.contagem = 0

    def __call__(self, *args, **kwargs):
        self.contagem += 1
        print(f"Chamada {self.contagem} de {self.func.__name__}")
        return self.func(*args, **kwargs)

@ContadorChamadas
def diga_oi():
    print("Oi!")

diga_oi()
diga_oi()
# Saída:
# Chamada 1 de diga_oi
# Oi!
# Chamada 2 de diga_oi
# Oi!

Note que usamos functools.update_wrapper para copiar os metadados da função original para a instância da classe, garantindo que a introspecção funcione.

Decorators de classe também podem aceitar argumentos se implementarmos __init__ com parâmetros extras e retornarmos uma instância que é um callable.

Empilhamento

Você pode aplicar múltiplos decorators a uma mesma função. A ordem de aplicação é de baixo para cima: o decorator mais próximo da função é aplicado primeiro, depois o próximo, e assim por diante. Ao chamar a função, a ordem de execução é de cima para baixo (o decorator mais externo executa primeiro).

Exemplo:

import functools

def decorator_a(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Entrando em A")
        resultado = func(*args, **kwargs)
        print("Saindo de A")
        return resultado
    return wrapper

def decorator_b(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Entrando em B")
        resultado = func(*args, **kwargs)
        print("Saindo de B")
        return resultado
    return wrapper

@decorator_a
@decorator_b
def mensagem(texto):
    print(f"Mensagem: {texto}")

mensagem("Olá")
# Saída:
# Entrando em A
# Entrando em B
# Mensagem: Olá
# Saindo de B
# Saindo de A

O empilhamento é útil para combinar comportamentos como logging, timing, validação, etc.

Exemplos reais

Decorators são amplamente usados em frameworks e bibliotecas. Aqui estão dois exemplos práticos.

Cache (Memoização)

Um decorator que armazena resultados de chamadas anteriores para evitar recálculos.

import functools

def cache(func):
    memo = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args in memo:
            print("Usando cache")
            return memo[args]
        resultado = func(*args)
        memo[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 otimizado)

Timer para medir tempo de execução

import functools
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 tarefa_pesada():
    time.sleep(2)
    return "Pronto"

print(tarefa_pesada())

Outros exemplos incluem validação de argumentos, controle de acesso, logging automático, etc.

Boas práticas

  • Sempre use @functools.wraps para preservar metadados.
  • Evite decorators que alteram a assinatura da função (argumentos) sem necessidade.
  • Documente o comportamento do decorator.
  • Prefira decorators de função simples a menos que precise de estado.

Referências

Exercícios

  1. Crie um decorator de classe chamado Logger que registra (print) o nome da função e seus argumentos toda vez que a função for chamada. Use functools.update_wrapper.

    ✓ Resposta:
    import functools
    
    class Logger:
        def __init__(self, func):
            functools.update_wrapper(self, func)
            self.func = func
    
        def __call__(self, *args, **kwargs):
            print(f"Chamando {self.func.__name__} com args={args}, kwargs={kwargs}")
            return self.func(*args, **kwargs)
    
    @Logger
    def soma(a, b):
        return a + b
    
    print(soma(2, 3))
    
  2. Empilhe dois decorators: um que converte o retorno para maiúsculas e outro que adiciona pontos de exclamação no final. Aplique na ordem: primeiro maiúsculas, depois exclamação. Teste com uma função que retorna uma string.

    ✓ Resposta:
    import functools
    
    def maiusculas(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            resultado = func(*args, **kwargs)
            return resultado.upper()
        return wrapper
    
    def exclamacao(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            resultado = func(*args, **kwargs)
            return resultado + "!!!"
        return wrapper
    
    @exclamacao
    @maiusculas
    def saudacao(nome):
        return f"Olá, {nome}"
    
    print(saudacao("João"))  # OLÁ, JOÃO!!!
    
  3. Crie um decorator repetir(n) que aceita um argumento n e repete a chamada da função decorada n vezes, retornando uma lista com os resultados.

    ✓ Resposta:
    import functools
    
    def repetir(n):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                resultados = []
                for _ in range(n):
                    resultados.append(func(*args, **kwargs))
                return resultados
            return wrapper
        return decorator
    
    @repetir(3)
    def diga_oi():
        return "Oi"
    
    print(diga_oi())  # ['Oi', 'Oi', 'Oi']
    
  4. Usando functools.wraps, crie um decorator que mede o tempo de execução e exibe o resultado em segundos com 3 casas decimais. Teste com uma função que dorme 0.5 segundos.

    ✓ Resposta:
    import functools
    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:.3f} segundos")
            return resultado
        return wrapper
    
    @timer
    def esperar():
        time.sleep(0.5)
        return "Feito"
    
    print(esperar())
    
  5. Implemente um decorator de classe ContadorInstancias que conta quantas instâncias de uma classe foram criadas. Dica: decore o método __init__ da classe. (Pode usar um decorator de função normal aplicado ao método.)

    ✓ Resposta:
    import functools
    
    def conta_init(cls):
        original_init = cls.__init__
        @functools.wraps(original_init)
        def novo_init(self, *args, **kwargs):
            novo_init.contagem += 1
            print(f"Instância {novo_init.contagem} criada")
            return original_init(self, *args, **kwargs)
        novo_init.contagem = 0
        cls.__init__ = novo_init
        return cls
    
    @conta_init
    class Pessoa:
        def __init__(self, nome):
            self.nome = nome
    
    p1 = Pessoa("Ana")
    p2 = Pessoa("João")
    print(Pessoa.__init__.contagem)  # 2