PythonDecoradores

Decoradores

Decoradores em Python: Um Guia Completo para Iniciantes

Olá, dev! Vamos desvendar um dos recursos mais elegantes e poderosos do Python: os decoradores. Pode parecer mágica no começo, mas prometo que ao final deste guia você estará criando seus próprios decoradores com confiança.

O que é um Decorador?

Um decorador é uma função que modifica o comportamento de outra função sem alterar seu código-fonte. Pense nele como um "enfeite" que adiciona funcionalidades extras.

Na prática, um decorador:

  • Recebe uma função como argumento
  • Cria uma nova função que "envolve" a original
  • Retorna essa nova função

Funções como Objetos

Antes de entender decoradores, precisamos lembrar que em Python funções são objetos de primeira classe. Isso significa que podemos:

def saudacao():
    return "Olá, mundo!"

# Atribuir função a uma variável
minha_funcao = saudacao
print(minha_funcao())  # Olá, mundo!

# Passar função como argumento
def executar(funcao):
    return funcao()

print(executar(saudacao))  # Olá, mundo!

# Retornar função
def criar_saudacao():
    def saudacao_personalizada(nome):
        return f"Olá, {nome}!"
    return saudacao_personalizada

minha_saudacao = criar_saudacao()
print(minha_saudacao("Ana"))  # Olá, Ana!

A Sintaxe @decorator

A forma mais comum de usar decoradores é com o símbolo @:

@meu_decorador
def minha_funcao():
    # código aqui
    pass

Isso é equivalente a:

def minha_funcao():
    pass

minha_funcao = meu_decorador(minha_funcao)

Criando seu Primeiro Decorador

Vamos criar um decorador simples que imprime mensagens antes e depois da função:

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

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

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

Decorador para Funções com Argumentos

Se sua função original recebe argumentos, o wrapper precisa aceitá-los:

def decorador_com_argumentos(func):
    def wrapper(*args, **kwargs):
        print(f"Chamando função com argumentos: {args}, {kwargs}")
        resultado = func(*args, **kwargs)
        print(f"Resultado: {resultado}")
        return resultado
    return wrapper

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

somar(3, 5)
# Saída:
# Chamando função com argumentos: (3, 5), {}
# Resultado: 8

Decoradores com Argumentos

Às vezes queremos passar argumentos para o próprio decorador. Para isso, criamos uma função que retorna um decorador:

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 dizer_ola():
    print("Olá!")

dizer_ola()
# Olá!
# Olá!
# Olá!

@functools.wraps

Quando usamos decoradores, perdemos metadados importantes da função original (nome, docstring, etc.). O functools.wraps resolve isso:

import functools

def decorador_educado(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Por favor,")
        resultado = func(*args, **kwargs)
        print("Obrigado!")
        return resultado
    return wrapper

@decorador_educado
def somar(a, b):
    """Soma dois números"""
    return a + b

print(somar.__name__)  # somar (sem wraps seria 'wrapper')
print(somar.__doc__)   # Soma dois números (sem wraps seria None)

Sempre use @functools.wraps em seus decoradores!

Exemplos Práticos

1. Decorador Timer (Medir 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 calcular_soma(n):
    soma = 0
    for i in range(n):
        soma += i
    return soma

resultado = calcular_soma(1000000)
print(f"Resultado: {resultado}")
# Exemplo de saída:
# calcular_soma levou 0.0342 segundos
# Resultado: 499999500000

2. Decorador de Log

import functools
from datetime import datetime

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] Executando {func.__name__}")
        print(f"    Argumentos: args={args}, kwargs={kwargs}")
        resultado = func(*args, **kwargs)
        print(f"    Resultado: {resultado}")
        return resultado
    return wrapper

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

dividir(10, 2)
# Saída:
# [2024-01-15 14:30:00] Executando dividir
#     Argumentos: args=(10, 2), kwargs={}
#     Resultado: 5.0

3. Decorador de Validação

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

@validar_positivo
def raiz_quadrada(n):
    return n ** 0.5

print(raiz_quadrada(9))  # 3.0
# print(raiz_quadrada(-4))  # ValueError: Argumento negativo não permitido: -4

Erros Comuns e Dicas

❌ Erro: Esquecer de retornar o resultado da função original

# ERRADO
def decorador_errado(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)  # Não retorna o resultado!
    return wrapper

# CERTO
def decorador_certo(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)  # Retorna o resultado
    return wrapper

❌ Erro: Esquecer @functools.wraps

# ERRADO - perde metadados
def decorador(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# CERTO - preserva metadados
import functools

def decorador(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

❌ Erro: Confundir decoradores com argumentos

# ERRADO - não é assim que se faz decorador com argumento
@decorador(argumento)  # Isso chama decorador(argumento) antes de receber a função

# CERTO - precisa de uma função extra
def decorador(argumento):
    def decorador_real(func):
        def wrapper(*args, **kwargs):
            # usa argumento aqui
            return func(*args, **kwargs)
        return wrapper
    return decorador_real

✅ Dica: Use decoradores para separar responsabilidades

# Em vez de misturar lógica de negócio com logging:
def processar_dados(dados):
    print(f"Processando dados: {dados}")  # misturado
    # lógica real...
    return dados * 2

# Separe as responsabilidades:
def log_processamento(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Processando dados: {args}")
        return func(*args, **kwargs)
    return wrapper

@log_processamento
def processar_dados(dados):
    # apenas lógica de negócio
    return dados * 2

✅ Dica: Empilhe decoradores

@timer
@log
def minha_funcao():
    # código
    pass

# A ordem importa! Executa de baixo para cima:
# 1. log envolve minha_funcao
# 2. timer envolve o resultado

Resumo

  • Decoradores são funções que modificam outras funções
  • Use @functools.wraps para preservar metadados
  • Lembre-se de retornar o resultado da função original
  • Decoradores com argumentos precisam de um nível extra de função
  • São ótimos para: timer, log, validação, autenticação, cache

Pratique criando seus próprios decoradores! Comece com exemplos simples e vá aumentando a complexidade. Em pouco tempo você estará usando decoradores como um profissional.

Bons estudos e mãos ao código! 🐍✨