Decoradores e Closures

Decoradores são um dos recursos mais elegantes do Python. Eles permitem "adicionar extras" a funções sem modificar o código original — adicionar logging, temporização, verificações de permissão... Muitos frameworks e bibliotecas usam decoradores extensivamente. Entendê-los é um marco importante no domínio do Python.


1. Closures

Closures são a base dos decoradores. Simplificando: uma função interna lembra variáveis da função externa, mesmo após a função externa ter terminado sua execução.

PYTHON
def make_multiplier(n):
    """Cria uma função de multiplicação"""
    def multiplier(x):
        return x * n        # Função interna lembra a variável externa n
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))            # 10
print(triple(5))            # 15
print(double(10))           # 20

Mesmo que double e triple venham da mesma função de fábrica make_multiplier, cada uma lembra valores diferentes de n (2 e 3). Isto é um closure — uma função + as variáveis externas que ela lembra.

💡 Casos de uso de closures: 1) Funções de fábrica (criando funções configuradas diferentemente como acima). 2) Decoradores (implementação padrão). 3) Salvar contexto em funções de callback.


2. Básico de Decoradores

Um decorador é uma função que recebe uma função como argumento e retorna uma nova função:

PYTHON
def my_decorator(func):
    def wrapper():
        print("=== Antes da execução da função ===")
        func()
        print("=== Depois da execução da função ===")
    return wrapper

@my_decorator
def say_hello():
    print("Olá!")

say_hello()

Saída:

TEXT
=== Antes da execução da função ===
Olá!
=== Depois da execução da função ===

@my_decorator equivale a say_hello = my_decorator(say_hello). O açúcar sintático torna o código mais limpo.

Exemplo: Decorador de Logging (Dificuldade ⭐⭐)

PYTHON
def log_call(func):
    """Registra chamadas de função"""
    def wrapper(*args, **kwargs):
        print(f"[LOG] Chamando {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} retornou {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

@log_call
def greet(name, greeting="Olá"):
    return f"{greeting}, {name}!"

print(add(3, 5))
print(greet("João Silva"))
▶ Experimente

Saída:

TEXT
[LOG] Chamando add((3, 5), {})
[LOG] add retornou 8
8
[LOG] Chamando greet(('João Silva',), {'greeting': 'Olá'})
[LOG] greet retornou Olá, João Silva!

3. functools.wraps: Preservando Informações da Função Original

Decoradores "substituem" a função original, fazendo com que o nome e o docstring da função sejam perdidos. @wraps resolve isso:

PYTHON
from functools import wraps

def my_decorator(func):
    @wraps(func)                    # Preserva informações da função original
    def wrapper(*args, **kwargs):
        """Função interna do decorador"""
        print("Antes da execução da função...")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """Uma função de saudação simples"""
    print("Olá!")

print(say_hello.__name__)           # say_hello (sem @wraps, mostraria wrapper)
print(say_hello.__doc__)            # Uma função de saudação simples (sem @wraps, mostraria função interna do decorador)
💡 Sempre adicione @wraps ao escrever decoradores. Sem ele, o nome e a documentação da função decorada são perdidos, dificultando a depuração. Importe wraps de functools e aplique @wraps(func) à função wrapper interna.


4. Decoradores Parametrizados

Às vezes, o próprio decorador precisa de parâmetros:

PYTHON
from functools import wraps

def repeat(times=2):
    """Repete a execução um número especificado de vezes"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Olá, {name}!")

greet("João Silva")

Saída:

TEXT
Olá, João Silva!
Olá, João Silva!
Olá, João Silva!

Exemplo: Decorador de Temporização (Dificuldade ⭐⭐⭐)

PYTHON
import time
from functools import wraps

def timer(func):
    """Calcula o tempo de execução da função"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"[Timer] {func.__name__} levou {elapsed:.4f} segundos")
        return result
    return wrapper

@timer
def funcao_lenta():
    total = 0
    for i in range(1000000):
        total += i
    return total

result = funcao_lenta()
print(f"Resultado: {result}")
▶ Experimente

Saída (tempo varia por computador):

TEXT
[Timer] funcao_lenta levou 0.0452 segundos
Resultado: 499999500000

5. Combinando Múltiplos Decoradores

Uma função pode ter vários decoradores empilhados. A ordem de execução é de baixo para cima:

PYTHON
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def hello():
    return "Olá"

print(hello())              # <b><i>Olá</i></b>
# Ordem de execução: primeiro @italic, depois @bold

# Equivalente a: hello = bold(italic(hello))
💡 Múltiplos decoradores executam de baixo para cima. Pense na função sendo "empacotada" camada por camada — o decorador inferior está mais próximo da função original e executa primeiro. Entender esta ordem evita confusão com múltiplos decoradores.


Casos de Uso Comuns


❓ Perguntas Frequentes

P: *args e kwargs são necessários em decoradores? R: Se seu decorador envolve funções com um número desconhecido de parâmetros, sim — use *args, kwargs para passar todos os argumentos. Se estiver envolvendo apenas funções com uma assinatura específica, você pode nomear parâmetros específicos, mas isso é menos genérico. Decoradores genéricos quase sempre usam *args, **kwargs. P: Qual a relação entre decoradores e closures? R: Decoradores são implementados usando closures. Um closure é "uma função que lembra variáveis externas." Decoradores aproveitam closures para que a função wrapper interna possa acessar o parâmetro func passado da função externa. P: Quais decoradores úteis vêm com Python? R: @property (acesso a atributos), @staticmethod (métodos estáticos), @classmethod (métodos de classe), @functools.lru_cache() (cache), @functools.wraps (preservar informações da função), @dataclass (classes de dados). Todos estão na biblioteca padrão.


📖 Resumo


📝 Atividades

  1. Iniciante (Dificuldade ⭐): Escreva um decorador print_call que imprime "Chamando função xxx" quando a função executa.

  2. Intermediário (Dificuldade ⭐⭐): Escreva um decorador retry(max_attempts=3) que tenta novamente automaticamente o número especificado de vezes quando uma função levanta uma exceção. Se as tentativas se esgotarem, levante a exceção final. Dica: Use um laço for + try-except no wrapper.

  3. Avançado (Dificuldade ⭐⭐⭐): Escreva um decorador parametrizado cache_result(ttl=60) que armazena em cache o valor de retorno da função. Dentro dos segundos TTL, chamar com os mesmos parâmetros retorna o resultado em cache; além do TTL, recalcula o valor.

100%