Decorators e Generators são recursos avançados do Python que separam programadores intermediários dos profissionais. Decorators permitem modificar o comportamento de funções de forma elegante, enquanto Generators criam iteradores eficientes em memória. Neste guia completo, você dominará ambos os conceitos com exemplos práticos do mundo real.

🎨 O Que São Decorators?

Decorators são funções que modificam o comportamento de outras funções sem alterar seu código. Pense neles como "wrappers" ou "embalagens" que adicionam funcionalidades extras.

# Decorator simples
def meu_decorator(funcao):
    def wrapper():
        print("🚀 Antes da função")
        funcao()
        print("✅ Depois da função")
    return wrapper

@meu_decorator
def saudacao():
    print("Olá, Universo Python!")

# Chamar a função decorada
saudacao()
# Output:
# 🚀 Antes da função
# Olá, Universo Python!
# ✅ Depois da função

O símbolo @ é apenas "açúcar sintático" para:

# Isso:
@meu_decorator
def saudacao():
    print("Olá!")

# É equivalente a:
def saudacao():
    print("Olá!")
saudacao = meu_decorator(saudacao)

📝 Criando Decorators

Decorator com Argumentos da Função

def log_chamada(funcao):
    """Registra quando uma função é chamada"""
    def wrapper(*args, **kwargs):
        print(f"📞 Chamando: {funcao.__name__}")
        print(f"   Args: {args}")
        print(f"   Kwargs: {kwargs}")
        
        resultado = funcao(*args, **kwargs)
        
        print(f"   Retorno: {resultado}")
        return resultado
    return wrapper

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

@log_chamada
def saudacao(nome, saudacao="Olá"):
    return f"{saudacao}, {nome}!"

print(somar(10, 5))
print(saudacao("Python", saudacao="Bem-vindo"))

Preservando Metadados com functools.wraps

from functools import wraps

def meu_decorator(funcao):
    @wraps(funcao)  # Preserva __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        """Wrapper interno"""
        return funcao(*args, **kwargs)
    return wrapper

@meu_decorator
def minha_funcao():
    """Documentação original"""
    pass

print(minha_funcao.__name__)  # minha_funcao (não 'wrapper')
print(minha_funcao.__doc__)   # Documentação original

Decorator com Parâmetros

from functools import wraps

def repetir(vezes):
    """Decorator que repete a função N vezes"""
    def decorator(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            resultados = []
            for _ in range(vezes):
                resultado = funcao(*args, **kwargs)
                resultados.append(resultado)
            return resultados
        return wrapper
    return decorator

@repetir(vezes=3)
def dizer_oi():
    print("Oi!")
    return "disse oi"

resultados = dizer_oi()
# Output: Oi! (3 vezes)
print(resultados)  # ['disse oi', 'disse oi', 'disse oi']

⚡ Decorators Úteis na Prática

Timer - Medir Tempo de Execução

import time
from functools import wraps

def timer(funcao):
    """Mede o tempo de execução de uma função"""
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = funcao(*args, **kwargs)
        fim = time.perf_counter()
        
        duracao = fim - inicio
        print(f"⏱️ {funcao.__name__} executou em {duracao:.4f}s")
        return resultado
    return wrapper

@timer
def processar_dados():
    time.sleep(1)  # Simular processamento
    return "Dados processados"

processar_dados()  # ⏱️ processar_dados executou em 1.0012s

Cache/Memoization

from functools import wraps

def cache(funcao):
    """Cache simples para resultados de função"""
    memoria = {}
    
    @wraps(funcao)
    def wrapper(*args):
        if args in memoria:
            print(f"📦 Cache hit para {args}")
            return memoria[args]
        
        resultado = funcao(*args)
        memoria[args] = resultado
        print(f"💾 Armazenando em cache: {args}")
        return resultado
    return wrapper

@cache
def fibonacci(n):
    """Calcula Fibonacci (recursivo)"""
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # Muito mais rápido com cache!

# Python 3.9+ tem lru_cache built-in:
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci_otimizado(n):
    if n < 2:
        return n
    return fibonacci_otimizado(n-1) + fibonacci_otimizado(n-2)

Retry - Tentar Novamente em Caso de Erro

import time
from functools import wraps

def retry(tentativas=3, delay=1, excecoes=(Exception,)):
    """Tenta executar a função múltiplas vezes"""
    def decorator(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            ultima_excecao = None
            
            for tentativa in range(1, tentativas + 1):
                try:
                    return funcao(*args, **kwargs)
                except excecoes as e:
                    ultima_excecao = e
                    print(f"⚠️ Tentativa {tentativa}/{tentativas} falhou: {e}")
                    
                    if tentativa < tentativas:
                        print(f"   Aguardando {delay}s...")
                        time.sleep(delay)
            
            raise ultima_excecao
        return wrapper
    return decorator

@retry(tentativas=3, delay=1)
def buscar_api():
    import random
    if random.random() < 0.7:  # 70% de chance de falhar
        raise ConnectionError("Falha na conexão")
    return {"status": "success"}

try:
    resultado = buscar_api()
    print(f"✅ Sucesso: {resultado}")
except ConnectionError as e:
    print(f"❌ Falha após todas as tentativas: {e}")

Validador de Tipos

from functools import wraps

def validar_tipos(*tipos_args, **tipos_kwargs):
    """Valida tipos dos argumentos"""
    def decorator(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            # Validar args posicionais
            for arg, tipo in zip(args, tipos_args):
                if not isinstance(arg, tipo):
                    raise TypeError(
                        f"Esperado {tipo.__name__}, recebido {type(arg).__name__}"
                    )
            
            # Validar kwargs
            for nome, valor in kwargs.items():
                if nome in tipos_kwargs:
                    tipo = tipos_kwargs[nome]
                    if not isinstance(valor, tipo):
                        raise TypeError(
                            f"{nome}: esperado {tipo.__name__}, "
                            f"recebido {type(valor).__name__}"
                        )
            
            return funcao(*args, **kwargs)
        return wrapper
    return decorator

@validar_tipos(str, int, idade=int)
def criar_usuario(nome, idade):
    return {"nome": nome, "idade": idade}

print(criar_usuario("Ana", 25))  # OK
# criar_usuario(123, "25")  # TypeError!

Autenticação/Autorização

from functools import wraps

# Simular usuário logado
usuario_atual = {"nome": "Admin", "role": "admin"}

def requer_login(funcao):
    """Verifica se usuário está logado"""
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        if not usuario_atual:
            raise PermissionError("❌ Faça login primeiro!")
        return funcao(*args, **kwargs)
    return wrapper

def requer_role(*roles_permitidas):
    """Verifica se usuário tem permissão"""
    def decorator(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            if usuario_atual.get("role") not in roles_permitidas:
                raise PermissionError(
                    f"❌ Acesso negado. Requer: {roles_permitidas}"
                )
            return funcao(*args, **kwargs)
        return wrapper
    return decorator

@requer_login
@requer_role("admin", "moderador")
def deletar_usuario(user_id):
    return f"✅ Usuário {user_id} deletado"

print(deletar_usuario(42))

🔄 O Que São Generators?

Generators são funções que produzem uma sequência de valores sob demanda, usando yield em vez de return. Eles são extremamente eficientes em memória porque geram valores um de cada vez.

# Função normal - armazena tudo na memória
def lista_quadrados(n):
    resultado = []
    for i in range(n):
        resultado.append(i ** 2)
    return resultado

# Generator - gera sob demanda
def generator_quadrados(n):
    for i in range(n):
        yield i ** 2

# Comparação de memória
import sys

lista = lista_quadrados(1000000)
gen = generator_quadrados(1000000)

print(f"Lista: {sys.getsizeof(lista):,} bytes")  # ~8MB
print(f"Generator: {sys.getsizeof(gen)} bytes")  # ~120 bytes!

📝 Criando Generators

Generator Simples

def contagem_regressiva(n):
    """Generator de contagem regressiva"""
    print("🚀 Iniciando contagem...")
    while n > 0:
        yield n
        n -= 1
    print("🔥 Decolagem!")

# Usar com for
for numero in contagem_regressiva(5):
    print(numero)

# Ou manualmente com next()
gen = contagem_regressiva(3)
print(next(gen))  # 3
print(next(gen))  # 2
print(next(gen))  # 1
# print(next(gen))  # StopIteration

Generator Infinito

def numeros_pares():
    """Generator infinito de números pares"""
    n = 0
    while True:
        yield n
        n += 2

# Usar com cuidado!
pares = numeros_pares()
for _ in range(10):
    print(next(pares), end=" ")
# 0 2 4 6 8 10 12 14 16 18

Generator com Múltiplos Yields

def ler_arquivo_grande(caminho, chunk_size=1024):
    """Lê arquivo grande em chunks"""
    with open(caminho, 'r') as arquivo:
        while True:
            chunk = arquivo.read(chunk_size)
            if not chunk:
                break
            yield chunk

# Processar arquivo de 1GB sem carregar na memória
# for chunk in ler_arquivo_grande("arquivo_grande.txt"):
#     processar(chunk)

🎯 Generator Expressions

Similar a list comprehension, mas com parênteses:

# List comprehension - cria lista completa
lista = [x**2 for x in range(1000000)]

# Generator expression - gera sob demanda
gen = (x**2 for x in range(1000000))

# Comparação
import sys
print(f"Lista: {sys.getsizeof(lista):,} bytes")
print(f"Generator: {sys.getsizeof(gen)} bytes")

# Usar diretamente em funções
soma = sum(x**2 for x in range(1000))  # Sem [] extras
media = sum(x for x in range(100)) / 100

⚡ yield from - Delegação

def generator_aninhado():
    yield from range(3)      # 0, 1, 2
    yield from "abc"         # a, b, c
    yield from [10, 20, 30]  # 10, 20, 30

for item in generator_aninhado():
    print(item, end=" ")
# 0 1 2 a b c 10 20 30


# Exemplo prático: achatar listas
def achatar(lista):
    """Achata listas aninhadas"""
    for item in lista:
        if isinstance(item, list):
            yield from achatar(item)
        else:
            yield item

aninhada = [1, [2, 3, [4, 5]], 6, [7, 8]]
print(list(achatar(aninhada)))
# [1, 2, 3, 4, 5, 6, 7, 8]

🔧 Métodos de Generator

def generator_interativo():
    """Generator que aceita valores"""
    total = 0
    while True:
        valor = yield total
        if valor is None:
            break
        total += valor

gen = generator_interativo()
print(next(gen))      # Iniciar: 0
print(gen.send(10))   # Enviar 10: 10
print(gen.send(20))   # Enviar 20: 30
print(gen.send(5))    # Enviar 5: 35
# gen.send(None)      # Finalizar

🎯 Projeto Prático: Sistema de Pipeline de Dados

Vamos criar um sistema de processamento de dados usando decorators e generators, aplicando conceitos de POO:

import time
import random
from functools import wraps
from typing import Generator, Callable, Any, List
from dataclasses import dataclass

# ============================================
# DECORATORS DO SISTEMA
# ============================================

def timer(funcao):
    """Mede tempo de execução"""
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = funcao(*args, **kwargs)
        duracao = time.perf_counter() - inicio
        print(f"⏱️ {funcao.__name__}: {duracao:.4f}s")
        return resultado
    return wrapper


def log_pipeline(funcao):
    """Loga etapas do pipeline"""
    @wraps(funcao)
    def wrapper(*args, **kwargs):
        print(f"📍 Iniciando: {funcao.__name__}")
        resultado = funcao(*args, **kwargs)
        print(f"✅ Concluído: {funcao.__name__}")
        return resultado
    return wrapper


def retry_generator(tentativas=3, delay=0.5):
    """Retry para generators"""
    def decorator(funcao):
        @wraps(funcao)
        def wrapper(*args, **kwargs):
            for tentativa in range(tentativas):
                try:
                    yield from funcao(*args, **kwargs)
                    return
                except Exception as e:
                    print(f"⚠️ Erro na tentativa {tentativa+1}: {e}")
                    if tentativa < tentativas - 1:
                        time.sleep(delay)
            raise RuntimeError(f"Falhou após {tentativas} tentativas")
        return wrapper
    return decorator


# ============================================
# GENERATORS DO PIPELINE
# ============================================

@dataclass
class DadosBrutos:
    id: int
    valor: float
    timestamp: float


def gerar_dados(quantidade: int) -> Generator[DadosBrutos, None, None]:
    """Generator que simula fonte de dados"""
    for i in range(quantidade):
        yield DadosBrutos(
            id=i,
            valor=random.uniform(0, 100),
            timestamp=time.time()
        )
        time.sleep(0.01)  # Simular latência


def filtrar_dados(dados: Generator, minimo: float = 20) -> Generator:
    """Filtra dados abaixo do mínimo"""
    for dado in dados:
        if dado.valor >= minimo:
            yield dado


def transformar_dados(dados: Generator) -> Generator[dict, None, None]:
    """Transforma dados em dicionário processado"""
    for dado in dados:
        yield {
            "id": dado.id,
            "valor_original": dado.valor,
            "valor_processado": round(dado.valor * 1.1, 2),
            "categoria": "alto" if dado.valor > 50 else "baixo",
            "processado_em": time.time()
        }


def agrupar_lotes(dados: Generator, tamanho_lote: int = 10) -> Generator[List, None, None]:
    """Agrupa dados em lotes"""
    lote = []
    for dado in dados:
        lote.append(dado)
        if len(lote) >= tamanho_lote:
            yield lote
            lote = []
    
    if lote:  # Último lote parcial
        yield lote


# ============================================
# PIPELINE PRINCIPAL
# ============================================

class DataPipeline:
    """Pipeline de processamento de dados"""
    
    def __init__(self, nome: str):
        self.nome = nome
        self.etapas: List[Callable] = []
        self.estatisticas = {
            "total_processado": 0,
            "total_filtrado": 0,
            "lotes_gerados": 0
        }
    
    def adicionar_etapa(self, etapa: Callable) -> 'DataPipeline':
        """Adiciona etapa ao pipeline (fluent interface)"""
        self.etapas.append(etapa)
        return self
    
    @timer
    @log_pipeline
    def executar(self, dados_entrada: Generator) -> Generator:
        """Executa o pipeline completo"""
        dados = dados_entrada
        
        for etapa in self.etapas:
            dados = etapa(dados)
        
        return dados
    
    def processar_e_salvar(self, dados: Generator) -> List[dict]:
        """Processa e coleta todos os resultados"""
        resultados = []
        
        for lote in dados:
            self.estatisticas["lotes_gerados"] += 1
            
            for item in lote:
                resultados.append(item)
                self.estatisticas["total_processado"] += 1
        
        return resultados
    
    def relatorio(self):
        """Mostra relatório do pipeline"""
        print(f"\n📊 RELATÓRIO: {self.nome}")
        print("=" * 40)
        for chave, valor in self.estatisticas.items():
            print(f"  {chave}: {valor}")


# ============================================
# DECORATORS DE CLASSE
# ============================================

def singleton(cls):
    """Garante apenas uma instância da classe"""
    instancias = {}
    
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instancias:
            instancias[cls] = cls(*args, **kwargs)
        return instancias[cls]
    
    return get_instance


@singleton
class ConfigManager:
    """Gerenciador de configurações (singleton)"""
    
    def __init__(self):
        self.config = {
            "pipeline_ativo": True,
            "lote_tamanho": 5,
            "valor_minimo": 25
        }
    
    def get(self, chave):
        return self.config.get(chave)


# ============================================
# EXECUTAR DEMONSTRAÇÃO
# ============================================

if __name__ == "__main__":
    print("=" * 60)
    print("🚀 SISTEMA DE PIPELINE DE DADOS")
    print("=" * 60)
    
    # Obter configurações (singleton)
    config = ConfigManager()
    print(f"\n⚙️ Configurações: {config.config}")
    
    # Criar pipeline
    pipeline = DataPipeline("Pipeline de Vendas")
    
    # Configurar etapas
    pipeline.adicionar_etapa(
        lambda d: filtrar_dados(d, config.get("valor_minimo"))
    ).adicionar_etapa(
        transformar_dados
    ).adicionar_etapa(
        lambda d: agrupar_lotes(d, config.get("lote_tamanho"))
    )
    
    # Gerar dados de entrada
    dados_entrada = gerar_dados(50)
    
    # Executar pipeline
    print("\n" + "-" * 60)
    dados_processados = pipeline.executar(dados_entrada)
    
    # Coletar resultados
    resultados = pipeline.processar_e_salvar(dados_processados)
    
    # Relatório
    pipeline.relatorio()
    
    # Mostrar alguns resultados
    print("\n📄 Primeiros 3 resultados:")
    for item in resultados[:3]:
        print(f"  {item}")
    
    print(f"\n✅ Total de itens processados: {len(resultados)}")

💡 Quando Usar Cada Um

Use Decorators Para:

  • ✅ Logging e monitoramento
  • ✅ Cache/memoization
  • ✅ Autenticação e autorização
  • ✅ Validação de entrada
  • ✅ Retry em caso de falha
  • ✅ Medir tempo de execução
  • ✅ Adicionar funcionalidade sem modificar código

Use Generators Para:

  • ✅ Processar arquivos grandes
  • ✅ Streaming de dados
  • ✅ Pipelines de dados
  • ✅ Sequências infinitas
  • ✅ Economizar memória
  • ✅ Lazy evaluation

📚 Decorators Built-in do Python

class MinhaClasse:
    
    @property
    def valor(self):
        """Getter como propriedade"""
        return self._valor
    
    @valor.setter
    def valor(self, novo_valor):
        """Setter"""
        self._valor = novo_valor
    
    @staticmethod
    def metodo_estatico():
        """Não recebe self"""
        return "Estático"
    
    @classmethod
    def metodo_classe(cls):
        """Recebe a classe como primeiro argumento"""
        return cls.__name__


# Do functools
from functools import lru_cache, cached_property, singledispatch

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

🚀 Próximos Passos

  • Funções - Base para decorators
  • POO - Decorators de classe
  • List Comprehension - Comparar com generators
  • Try/Except - Decorators de retry
  • Arquivos - Generators para arquivos grandes
  • Async/Await - Programação assíncrona
  • Context Managers - with statement customizado

Quer dominar Python avançado? Confira nosso curso completo com projetos profissionais!

📝 Resumo

  • ✅ Decorators - modificam comportamento de funções
  • ✅ @wraps para preservar metadados
  • ✅ Decorators práticos: timer, cache, retry, auth
  • ✅ Decorators com parâmetros
  • ✅ Generators - produzem valores sob demanda
  • ✅ yield vs return
  • ✅ Generator expressions
  • ✅ yield from para delegação
  • ✅ Projeto: Pipeline de dados completo
  • ✅ Singleton decorator
  • ✅ Decorators built-in do Python

Decorators e Generators são ferramentas poderosas que elevam seu código Python a outro nível. Domine-os e você estará pronto para criar aplicações profissionais e eficientes!