Decorators são um dos recursos mais poderosos e elegantes do Python. Eles permitem modificar ou estender o comportamento de funções ou métodos sem alterar seu código fonte. Nesta aula, você entenderá o que são decorators, como funcionam por baixo dos panos e como aplicá-los em situações reais.

Um decorator é essencialmente uma função que recebe outra função como argumento e retorna uma nova função que geralmente adiciona alguma funcionalidade. A sintaxe com o símbolo @ torna isso ainda mais limpo e legível.

O que são

Em Python, funções são objetos de primeira classe: podem ser passadas como argumentos, retornadas de outras funções e atribuídas a variáveis. Um decorator é uma função que recebe uma função e retorna uma função modificada. Isso permite que você "envolva" uma função com código adicional que é executado antes, depois ou ao redor da função original.

Por exemplo, suponha que você queira medir o tempo de execução de uma função. Sem decorators, você precisaria adicionar código de temporização dentro de cada função. Com decorators, você cria uma função genérica que pode ser aplicada a qualquer função.

def meu_decorator(func):
    def wrapper():
        print("Antes da função")
        func()
        print("Depois da função")
    return wrapper

def dizer_oi():
    print("Oi!")

dizer_oi = meu_decorator(dizer_oi)
dizer_oi()
# Saída:
# Antes da função
# Oi!
# Depois da função

No exemplo acima, meu_decorator é um decorator que envolve a função dizer_oi com ações extras. Note que a função original é substituída pela função wrapper.

Sintaxe @

Python fornece um açúcar sintático para aplicar decorators: o símbolo @. Em vez de escrever dizer_oi = meu_decorator(dizer_oi), você pode simplesmente colocar @meu_decorator acima da definição da função.

@meu_decorator
def dizer_oi():
    print("Oi!")

dizer_oi()
# Saída:
# Antes da função
# Oi!
# Depois da função

Isso é equivalente à atribuição manual. A sintaxe @ torna o código mais limpo e explícito. É importante notar que o decorator é aplicado no momento da definição da função, não na chamada.

Decorators com argumentos

Às vezes, você precisa passar argumentos para o decorator. Por exemplo, um decorator que repete a execução de uma função um número específico de vezes. Para isso, você precisa de uma função que retorne um decorator.

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

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

cumprimentar("João")
# Saída:
# Olá, João!
# Olá, João!
# Olá, João!

Neste exemplo, repetir é uma função que recebe o argumento vezes e retorna um decorator. O decorator, por sua vez, recebe a função original e retorna a função wrapper que executa a repetição. A sintaxe @repetir(3) primeiro chama repetir(3) para obter o decorator e depois aplica-o à função.

É comum usar *args e **kwargs no wrapper para que o decorator funcione com funções de qualquer assinatura.

Casos de uso

Decorators são amplamente utilizados na prática. Alguns exemplos comuns incluem:

  • Logging: registrar chamadas de função, argumentos e resultados.
  • Timing: medir o tempo de execução de funções.
  • Validação: verificar argumentos ou permissões antes de executar a função.
  • Cache: armazenar resultados de funções caras para evitar recálculo.
  • Autenticação: em frameworks web, verificar se o usuário está logado.

Vamos ver um exemplo de decorator para logging:

import functools

def log(func):
    @functools.wraps(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

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

somar(3, 5)
# Saída:
# Chamando somar com args=(3, 5), kwargs={}
# somar retornou 8

Note o uso de functools.wraps para preservar os metadados da função original (como __name__ e __doc__). Isso é uma boa prática ao criar decorators.

Outro caso útil é um decorator que mede o tempo de execução:

import time
import functools

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"

tarefa_pesada()
# Saída: tarefa_pesada levou 2.0001 segundos

Decorators também são a base de muitos frameworks, como Flask (rotas) e Django (permissões). Eles permitem separar preocupações transversais do código principal, promovendo a reutilização e a clareza.

Boas práticas

  • Sempre use functools.wraps para preservar os metadados da função original.
  • Mantenha os decorators simples e focados em uma única responsabilidade.
  • Documente o comportamento do decorator, especialmente se ele modificar a assinatura da função.
  • Considere usar classes como decorators quando precisar de estado ou configuração mais complexa.

Referências

Exercícios

  1. Crie um decorator simples chamado maiusculas que converte o resultado de uma função em letras maiúsculas. A função decorada deve retornar uma string.

    ✓ Resposta:
    def maiusculas(func):
        def wrapper(*args, **kwargs):
            resultado = func(*args, **kwargs)
            return resultado.upper()
        return wrapper
    
    @maiusculas
    def saudacao(nome):
        return f"Olá, {nome}!"
    
    print(saudacao("Maria"))  # SAÍDA: OLÁ, MARIA!
  2. Crie um decorator que mede o tempo de execução de uma função e exibe o tempo em segundos com duas casas decimais. Use functools.wraps.

    ✓ Resposta:
    import time
    import functools
    
    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:.2f} segundos")
            return resultado
        return wrapper
    
    @timer
    def dormir():
        time.sleep(1.5)
        return "Acordei"
    
    print(dormir())  # Exibe o tempo e retorna "Acordei"
  3. Crie um decorator com argumento chamado validar_positivo que verifica se todos os argumentos numéricos passados para a função são positivos. Se algum não for, levanta uma exceção ValueError.

    ✓ Resposta:
    def validar_positivo(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
    
    @validar_positivo
    def multiplicar(a, b):
        return a * b
    
    print(multiplicar(3, 5))  # 15
    # print(multiplicar(-1, 5))  # ValueError
  4. Crie um decorator que repete a execução de uma função um número de vezes definido por um argumento do decorator. O decorator deve ser chamado como @repetir(3).

    ✓ Resposta:
    def repetir(vezes):
        def decorator(func):
            def wrapper(*args, **kwargs):
                for _ in range(vezes):
                    func(*args, **kwargs)
            return wrapper
        return decorator
    
    @repetir(3)
    def diga_oi():
        print("Oi!")
    
    diga_oi()  # Imprime "Oi!" três vezes
  5. Crie um decorator que registre em um arquivo de log as chamadas de função, incluindo timestamp, nome da função e argumentos. Use a biblioteca logging.

    ✓ Resposta:
    import logging
    import functools
    
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
    
    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 somar(a, b):
        return a + b
    
    somar(2, 3)  # Registra no log