Decoradores são um conceito avançado, mas extremamente útil. Eles são a chave para entender como muitos frameworks Python funcionam "por baixo dos panos" e para escrever código mais modular e limpo. Um decorador em Python é uma função que recebe outra função como argumento, adiciona alguma funcionalidade a ela e retorna uma nova função (ou a função modificada), sem alterar o código da função original diretamente.

Decoradores são amplamente usados para estender o comportamento de funções ou classes de forma limpa e reutilizável. Eles são basicamente uma "embalagem" que você coloca em volta de uma função para modificar ou aprimorar seu comportamento.

Conceito Fundamental: Funções Envoltas (Wrappers)

Para entender decoradores, imagine uma função que "envolve" outra.

def minha_funcao_original():
    print("Executando a função original.")

def funcao_envolvedora(func):
    def wrapper():
        print("Antes de chamar a função original.")
        func() # Chama a função original
        print("Depois de chamar a função original.")
    return wrapper

# Usando a função envolvedora manualmente
funcao_modificada = funcao_envolvedora(minha_funcao_original)
funcao_modificada()
# Saída:
# Antes de chamar a função original.
# Executando a função original.
# Depois de chamar a função original.

Neste exemplo, funcao_envolvedora é o que chamamos de função decoradora. Ela recebe minha_funcao_original, define uma nova função interna wrapper que adiciona comportamento antes e depois, e então retorna essa wrapper.

Sintaxe @ (Açúcar Sintático)

Python oferece uma sintaxe especial, o @ (arroba), que torna o uso de decoradores muito mais limpo e legível. O @ é apenas "açúcar sintático" para o processo de envolvimento manual que vimos acima.

A sintaxe:

@nome_do_decorador
def minha_funcao():
    pass

é equivalente a:

def minha_funcao():
    pass
minha_funcao = nome_do_decorador(minha_funcao)

Exemplo Prático de Decorador:

Vamos criar um decorador que mede o tempo de execução de uma função.

import time

def medir_tempo(func):
    """
    Um decorador que mede o tempo de execução de uma função.
    """
    def wrapper(*args, **kwargs): # *args e **kwargs permitem que a função decorada aceite qualquer argumento
        inicio = time.time()
        resultado = func(*args, **kwargs) # Executa a função original
        fim = time.time()
        print(f"A função '{func.__name__}' levou {fim - inicio:.4f} segundos para executar.")
        return resultado
    return wrapper

# Usando o decorador com a sintaxe @
@medir_tempo
def minha_funcao_lenta():
    """Uma função que simula um trabalho demorado."""
    print("Iniciando trabalho lento...")
    time.sleep(2) # Simula um atraso de 2 segundos
    print("Trabalho lento concluído.")
    return "Dados Processados"

@medir_tempo
def somar_numeros(a, b):
    print(f"Somando {a} e {b}...")
    return a + b

# Chamando as funções decoradas
valor_retornado = minha_funcao_lenta()
print(f"Função lenta retornou: {valor_retornado}")

soma = somar_numeros(10, 20)
print(f"Soma: {soma}")

Saída esperada:

Iniciando trabalho lento...
Trabalho lento concluído.
A função 'minha_funcao_lenta' levou 2.00xx segundos para executar.
Função lenta retornou: Dados Processados
Somando 10 e 20...
A função 'somar_numeros' levou 0.00xx segundos para executar.
Soma: 30

Por que usar Decoradores? (Casos de Uso Comuns)