Decorators são uma ferramenta poderosa em Python que permitem modificar ou estender o comportamento de funções e métodos sem alterar seu código fonte. Nesta aula avançada, vamos aprofundar em técnicas como o uso de functools.wraps para preservar metadados, criar decorators baseados em classes, empilhar múltiplos decorators e aplicar esses conceitos em exemplos reais do dia a dia.

Dominar esses tópicos é essencial para escrever código Python elegante, reutilizável e profissional. Vamos começar!

functools.wraps

Quando escrevemos um decorator, a função decorada perde seus metadados originais, como nome, docstring e módulo. Isso pode causar confusão durante depuração e inspeção. O decorator @functools.wraps resolve esse problema copiando os atributos da função original para a função wrapper.

Exemplo sem wraps:

def meu_decorator(func):
    def wrapper(*args, **kwargs):
        print("Antes")
        resultado = func(*args, **kwargs)
        print("Depois")
        return resultado
    return wrapper

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

print(saudacao.__name__)   # 'wrapper'
print(saudacao.__doc__)    # None

Com wraps, os metadados são preservados:

import functools

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

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

print(saudacao.__name__)   # 'saudacao'
print(saudacao.__doc__)    # 'Retorna uma saudação.'

Além de __name__ e __doc__, wraps também copia __module__, __annotations__ e __dict__. É uma boa prática sempre usar @functools.wraps em seus decorators.

Decorators de classe

Decorators não precisam ser funções; também podem ser classes. Uma classe decorator deve implementar o método __call__ para que sua instância seja chamável. Isso é útil quando precisamos manter estado ou configurar o decorator com parâmetros.

Exemplo de decorator de classe que conta chamadas:

class ContadorChamadas:
    def __init__(self, func):
        self.func = func
        self.contador = 0

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

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

diga_oi()  # Chamada 1 de diga_oi
diga_oi()  # Chamada 2 de diga_oi

Decorators de classe também podem aceitar argumentos. Para isso, a classe deve ser instanciada com os argumentos e depois o objeto é usado como decorator (chamando __call__ com a função). Exemplo:

class Repetir:
    def __init__(self, vezes):
        self.vezes = vezes

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(self.vezes):
                func(*args, **kwargs)
        return wrapper

@Repetir(3)
def diga_oi():
    print("Oi!")

diga_oi()  # Oi! Oi! Oi!

Empilhamento

Você pode aplicar múltiplos decorators a uma única função. Eles são aplicados de baixo para cima (da função para cima). A ordem importa e pode afetar o comportamento final.

Exemplo:

@decorator1
@decorator2
def funcao():
    pass

É equivalente a funcao = decorator1(decorator2(funcao)). Vamos ver um exemplo concreto com dois decorators simples:

def maiusculas(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        resultado = func(*args, **kwargs)
        return resultado.upper()
    return wrapper

def enfatizar(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        resultado = func(*args, **kwargs)
        return resultado + "!!!"
    return wrapper

@maiusculas
@enfatizar
def saudacao(nome):
    return f"Olá, {nome}"

print(saudacao("João"))  # OLÁ, JOÃO!!!

Note que @enfatizar é aplicado primeiro, depois @maiusculas. Se invertêssemos a ordem, o resultado seria "OLÁ, JOÃO!!!" mas com exclamações dentro da string. A ordem deve ser escolhida com cuidado.

Exemplos reais

Vamos ver alguns decorators úteis em projetos reais.

Logging: Registra chamadas de função com argumentos e resultados.

import logging

logging.basicConfig(level=logging.INFO)

def log(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"Resultado: {resultado}")
        return resultado
    return wrapper

@log
def soma(a, b):
    return a + b

soma(2, 3)  # INFO:root:Chamando soma com args=(2, 3), kwargs={} ... Resultado: 5

Timing: Mede o tempo de execução de uma função.

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 dorme():
    time.sleep(1)

dorme()  # dorme levou 1.0001 segundos

Autenticação: Simula verificação de usuário antes de executar uma funçã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

def usuario_autenticado():
    # Simulação
    return True

@requer_autenticacao
def dados_sensiveis():
    return "Dados secretos"

print(dados_sensiveis())

Cache: Armazena resultados de funções caras.

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

Boas práticas

Sempre use @functools.wraps para preservar metadados. Prefira decorators baseados em funções quando não precisar de estado; use classes quando precisar de estado ou configuração complexa. Documente seus decorators com docstrings claras. Lembre-se de que decorators podem ser aplicados a métodos de classe também, mas cuidado com o argumento self.

Referências

Exercícios

  1. Crie um decorator chamado executa_tres_vezes que executa a função decorada três vezes seguidas (sem argumentos). Use functools.wraps.

    ✓ Resposta:
    import functools
    
    def executa_tres_vezes(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(3):
                func(*args, **kwargs)
        return wrapper
    
  2. Implemente um decorator de classe Contador que conta quantas vezes a função decorada foi chamada. A classe deve armazenar o contador em um atributo contagem.

    ✓ Resposta:
    import functools
    
    class Contador:
        def __init__(self, func):
            self.func = func
            self.contagem = 0
    
        def __call__(self, *args, **kwargs):
            self.contagem += 1
            return self.func(*args, **kwargs)
    
  3. Empilhe os decorators @maiusculas e @enfatizar (definidos na aula) em uma função mensagem(texto) que retorna o texto. Teste ambas as ordens.

    ✓ Resposta:
    import functools
    
    def maiusculas(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            resultado = func(*args, **kwargs)
            return resultado.upper()
        return wrapper
    
    def enfatizar(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            resultado = func(*args, **kwargs)
            return resultado + "!!!"
        return wrapper
    
    @maiusculas
    @enfatizar
    def mensagem(texto):
        return texto
    
    print(mensagem("olá"))  # OLÁ!!!
    
    # Invertendo a ordem:
    @enfatizar
    @maiusculas
    def mensagem2(texto):
        return texto
    
    print(mensagem2("olá"))  # OLÁ!!!
    
  4. Crie um decorator validar_positivo que verifica se todos os argumentos numéricos são positivos (maiores que zero). Se algum não for, levanta ValueError. Use functools.wraps.

    ✓ Resposta:
    import functools
    
    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 {arg} não é positivo")
            for key, value in kwargs.items():
                if isinstance(value, (int, float)) and value <= 0:
                    raise ValueError(f"Argumento {key}={value} não é positivo")
            return func(*args, **kwargs)
        return wrapper
    
  5. Escreva um decorator logger que registra em um arquivo log.txt o nome da função e o timestamp de cada chamada. Use o módulo datetime.

    ✓ Resposta:
    import functools
    from datetime import datetime
    
    def logger(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            with open("log.txt", "a") as f:
                f.write(f"{datetime.now()} - {func.__name__} chamada\n")
            return func(*args, **kwargs)
        return wrapper