Python Admin

Dominando Decorators em Python: Criando, Empilhando e Parametrizando em Projetos Reais Já leu

Entendendo Decorators: O Conceito Fundamental Um decorator em Python é uma função que recebe outra função como argumento, adiciona alguma funcionalidade a ela sem modificar sua estrutura original, e retorna uma nova função com essa funcionalidade estendida. Pense em um decorator como um "embrulho" que você coloca ao redor de uma função — o conteúdo interno continua o mesmo, mas agora há camadas adicionais que podem fazer algo antes, depois ou até mesmo durante a execução. A razão pela qual decorators são tão poderosos é que eles respeitam o princípio DRY (Don't Repeat Yourself) e permitem separação de responsabilidades. Em vez de copiar código de validação, logging ou controle de tempo em múltiplas funções, você escreve isso uma única vez em um decorator e o reutiliza. Isso também torna seu código mais legível e fácil de manter. Vamos começar com um exemplo prático e simples. Imagine que você quer saber quanto tempo cada função leva para executar: python import time

Entendendo Decorators: O Conceito Fundamental

Um decorator em Python é uma função que recebe outra função como argumento, adiciona alguma funcionalidade a ela sem modificar sua estrutura original, e retorna uma nova função com essa funcionalidade estendida. Pense em um decorator como um "embrulho" que você coloca ao redor de uma função — o conteúdo interno continua o mesmo, mas agora há camadas adicionais que podem fazer algo antes, depois ou até mesmo durante a execução.

A razão pela qual decorators são tão poderosos é que eles respeitam o princípio DRY (Don't Repeat Yourself) e permitem separação de responsabilidades. Em vez de copiar código de validação, logging ou controle de tempo em múltiplas funções, você escreve isso uma única vez em um decorator e o reutiliza. Isso também torna seu código mais legível e fácil de manter.

Vamos começar com um exemplo prático e simples. Imagine que você quer saber quanto tempo cada função leva para executar:

import time
from functools import wraps

def cronometro(func):
    @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

@cronometro
def calcular_fibonacci(n):
    if n <= 1:
        return n
    return calcular_fibonacci(n-1) + calcular_fibonacci(n-2)

calcular_fibonacci(10)

Neste exemplo, o decorator cronometro envolve qualquer função e mede seu tempo de execução. O @wraps é importante — ele preserva o nome e a documentação originais da função decorada, evitando que você perca metadados.

Por que usar @wraps?

Sem @wraps, a função decorada perde seu __name__ original e documentação. O decorator @wraps (vindo de functools) copia esses metadados para a função wrapper, mantendo a identidade original intacta. Isso é especialmente útil em debugging e documentação automática.

Criando Seu Primeiro Decorator Funcional

Para dominar decorators, você precisa entender o fluxo de execução em camadas. Um decorator é, fundamentalmente, uma função que retorna outra função. Quando você usa @nome_decorator, Python automaticamente transforma funcao() em decorator(funcao)().

Vamos criar um decorator prático que valida se um parâmetro é um número positivo:

from functools import wraps

def valida_positivo(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Verifica se o primeiro argumento é positivo
        if args and not isinstance(args[0], (int, float)):
            raise TypeError(f"Primeiro argumento deve ser número, recebeu {type(args[0])}")
        if args and args[0] <= 0:
            raise ValueError("Primeiro argumento deve ser positivo")
        return func(*args, **kwargs)
    return wrapper

@valida_positivo
def calcular_raiz_quadrada(numero):
    return numero ** 0.5

print(calcular_raiz_quadrada(16))  # Saída: 4.0
print(calcular_raiz_quadrada(-5))  # Levanta ValueError

Este decorator valida a entrada antes de a função ser executada. É uma forma elegante de adicionar guardrails sem poluir a função principal com código de validação. Se você tiver dez funções que precisam dessa validação, aplica o decorator em cada uma — simples e consistente.

Anatomia de um Decorator

O padrão básico sempre segue esta estrutura:

def meu_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Lógica ANTES da função
        resultado = func(*args, **kwargs)
        # Lógica DEPOIS da função
        return resultado
    return wrapper

A *args captura argumentos posicionais e **kwargs captura argumentos nomeados. Isso garante que seu decorator funcione com qualquer assinatura de função.

Empilhando Decorators: Composição em Ação

Um dos recursos mais poderosos de Python é a capacidade de aplicar múltiplos decorators em uma única função. Quando você empilha decorators, eles são aplicados de baixo para cima — ou seja, o decorator mais próximo da função executa por último na cadeia, mas sua função wrapper executa primeiro.

Vamos criar dois decorators práticos e vê-los trabalhando juntos:

from functools import wraps
import time

def logger(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[LOG] Chamando {func.__name__} com args={args}, kwargs={kwargs}")
        resultado = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} retornou {resultado}")
        return resultado
    return wrapper

def tempo_execucao(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = func(*args, **kwargs)
        tempo = time.time() - inicio
        print(f"[TEMPO] {func.__name__} executou em {tempo:.6f}s")
        return resultado
    return wrapper

@logger
@tempo_execucao
def multiplicar(a, b):
    time.sleep(0.1)
    return a * b

multiplicar(3, 5)

Ordem de execução: Quando você chama multiplicar(3, 5), a execução ocorre assim:

  1. logger wrapper começa (imprime o log de entrada)
  2. tempo_execucao wrapper começa (inicia o cronômetro)
  3. A função original multiplicar executa
  4. tempo_execucao wrapper termina (imprime o tempo)
  5. logger wrapper termina (imprime o log de saída)

A ordem importa! Se você inverter os decorators (@tempo_execucao em cima de @logger), o log será envolvido pelo cronômetro, o que mudará o que está sendo medido. Teste ambas as formas e você verá a diferença.

Decorators com Estado Compartilhado

Às vezes, você quer que múltiplos decorators compartilhem informação. Embora raro, é possível:

def criar_contexto_decorador():
    contexto = {"chamadas": 0}

    def contador(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            contexto["chamadas"] += 1
            print(f"Chamada #{contexto['chamadas']}")
            return func(*args, **kwargs)
        return wrapper

    return contador

contador_global = criar_contexto_decorador()

@contador_global
def saudar(nome):
    print(f"Olá, {nome}!")

saudar("Alice")  # Chamada #1
saudar("Bob")    # Chamada #2

Parametrizando Decorators: Flexibilidade Total

Decorators parametrizados permitem que você customize seu comportamento no momento da aplicação. Em vez de @decorator, você usa @decorator(parametro). Isso adiciona uma camada extra de nesting, mas oferece flexibilidade imensa.

A estrutura de um decorator parametrizado é: uma função que retorna um decorator que retorna um wrapper:

from functools import wraps

def repetir(vezes):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            resultados = []
            for i in range(vezes):
                resultado = func(*args, **kwargs)
                resultados.append(resultado)
            return resultados
        return wrapper
    return decorator

@repetir(vezes=3)
def cumprimento(nome):
    return f"Olá, {nome}!"

print(cumprimento("Maria"))
# Saída: ['Olá, Maria!', 'Olá, Maria!', 'Olá, Maria!']

Neste exemplo, @repetir(vezes=3) cria um decorator que executa a função três vezes e retorna uma lista com todos os resultados. A parametrização torna o decorator reutilizável em contextos diferentes — você pode usar @repetir(vezes=5) em outra função.

Exemplo Prático: Decorator com Múltiplos Parâmetros

Vamos criar um decorator mais complexo que combina logging, retry e validação:

from functools import wraps
import time

def resiliente(tentativas=3, aguardar=1, log_erros=True):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ultima_excecao = None

            for tentativa in range(1, tentativas + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    ultima_excecao = e
                    if log_erros:
                        print(f"[ERRO] Tentativa {tentativa}/{tentativas} falhou: {e}")

                    if tentativa < tentativas:
                        print(f"[AGUARDANDO] {aguardar}s antes da próxima tentativa...")
                        time.sleep(aguardar)

            raise ultima_excecao
        return wrapper
    return decorator

@resiliente(tentativas=3, aguardar=0.5, log_erros=True)
def operacao_instavel(numero):
    import random
    if random.random() < 0.7:
        raise ConnectionError("Falha na conexão")
    return f"Sucesso com {numero}"

try:
    print(operacao_instavel(42))
except Exception as e:
    print(f"Falha final: {e}")

Este decorator é especialmente útil para operações que podem falhar temporariamente (chamadas a APIs, leitura de banco de dados) — ele tenta de novo automaticamente.

Decorators Parametrizados com Valores Padrão

Você pode tornar seus parâmetros opcionais:

from functools import wraps

def debug(prefixo="DEBUG"):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{prefixo}] Executando {func.__name__}")
            resultado = func(*args, **kwargs)
            print(f"[{prefixo}] Resultado: {resultado}")
            return resultado
        return wrapper
    return decorator

@debug()  # Usa prefixo padrão
def funcao_um():
    return "Um"

@debug(prefixo="INFO")  # Customiza o prefixo
def funcao_dois():
    return "Dois"

funcao_um()
funcao_dois()

Conclusão

Você aprendeu que decorators são uma ferramenta de composição funcional que permite adicionar comportamento a funções sem modificar seu código original — e que isso respeita o princípio de responsabilidade única. Em segundo lugar, empilhar decorators oferece elegância e modularidade, permitindo que você combine funcionalidades de forma clara e legível, desde que entenda a ordem de execução. Por fim, parametrizar decorators transforma-os em ferramentas altamente reutilizáveis, capazes de se adaptar a contextos diferentes sem duplicação de código.

A verdadeira maestria vem com a prática: escreva decorators para logging, validação, cache e tratamento de erros em seus projetos reais. Você descobrirá que muitos problemas de engenharia de código têm uma solução elegante através de decorators bem projetados.

Referências


Artigos relacionados