Closures e funções de fábrica

1. Introdução aos Conceitos Fundamentais

1.1. O que são funções de primeira classe em Python

Em Python, funções são cidadãs de primeira classe. Isso significa que podem ser atribuídas a variáveis, passadas como argumentos para outras funções, retornadas como valores e armazenadas em estruturas de dados. Essa característica é fundamental para entender closures e funções de fábrica.

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

# Função atribuída a uma variável
minha_funcao = saudacao
print(minha_funcao("Maria"))  # Olá, Maria!

# Função passada como argumento
def executar(funcao, argumento):
    return funcao(argumento)

print(executar(saudacao, "João"))  # Olá, João!

1.2. Definição de closure: função interna que "lembra" o escopo léxico

Uma closure é uma função interna que captura e mantém acesso a variáveis do escopo externo, mesmo após a função externa ter encerrado sua execução. Em termos práticos, a closure "lembra" do ambiente onde foi criada.

1.3. Diferença entre closures e funções aninhadas comuns

Nem toda função aninhada é uma closure. Uma função aninhada comum apenas acessa variáveis do escopo externo durante a execução da função externa. Uma closure, por outro lado, retém essas variáveis mesmo após a função externa terminar.

# Função aninhada comum (não é closure)
def externa_comum():
    mensagem = "Temporário"
    def interna():
        return mensagem + " (só durante execução)"
    return interna()  # Executa imediatamente

# Closure verdadeira
def externa_closure():
    mensagem = "Lembrado!"
    def interna():
        return mensagem + " (mesmo após externa terminar)"
    return interna  # Retorna a função, não o resultado

closure = externa_closure()
print(closure())  # Lembrado! (mesmo após externa terminar)

2. Anatomia de uma Closure

2.1. Estrutura básica: função externa retornando função interna

def criar_saudacao(saudacao_personalizada):
    def saudar(nome):
        return f"{saudacao_personalizada}, {nome}!"
    return saudar

saudar_ola = criar_saudacao("Olá")
saudar_bom_dia = criar_saudacao("Bom dia")

print(saudar_ola("Ana"))      # Olá, Ana!
print(saudar_bom_dia("Carlos"))  # Bom dia, Carlos!

2.2. O atributo __closure__ e células de variáveis livres

O Python armazena as variáveis capturadas pela closure no atributo __closure__. Cada célula contém uma variável livre.

def externa():
    x = 10
    y = 20
    def interna():
        return x + y
    return interna

closure = externa()
print(closure.__closure__)  # Mostra as células
print(closure.__closure__[0].cell_contents)  # 10
print(closure.__closure__[1].cell_contents)  # 20

2.3. Captura de variáveis mutáveis e imutáveis: armadilhas comuns

Variáveis imutáveis (como inteiros e strings) são capturadas por valor. Variáveis mutáveis (como listas e dicionários) são capturadas por referência, o que pode causar comportamentos inesperados.

# Armadilha comum com variáveis mutáveis
def criar_contadores():
    contadores = []
    for i in range(3):
        def contador():
            return i  # Captura i por referência
        contadores.append(contador)
    return contadores

contadores = criar_contadores()
print([c() for c in contadores])  # [2, 2, 2] (todos retornam 2!)

# Solução: capturar o valor atual
def criar_contadores_corrigido():
    contadores = []
    for i in range(3):
        def contador(valor=i):  # Valor padrão captura o valor atual
            return valor
        contadores.append(contador)
    return contadores

contadores_corrigido = criar_contadores_corrigido()
print([c() for c in contadores_corrigido])  # [0, 1, 2]

3. Funções de Fábrica (Factory Functions)

3.1. Conceito: funções que criam e retornam outras funções

Funções de fábrica são funções que geram e retornam outras funções, geralmente personalizadas com base em parâmetros fornecidos.

3.2. Exemplo prático: fábrica de funções matemáticas

def criar_multiplicador(fator):
    def multiplicar(numero):
        return numero * fator
    return multiplicar

dobro = criar_multiplicador(2)
triplo = criar_multiplicador(3)

print(dobro(5))   # 10
print(triplo(5))  # 15

# Fábrica de operações matemáticas
def criar_operacao(operacao):
    def operar(a, b):
        if operacao == 'soma':
            return a + b
        elif operacao == 'subtracao':
            return a - b
        elif operacao == 'multiplicacao':
            return a * b
        elif operacao == 'divisao':
            return a / b if b != 0 else "Erro: divisão por zero"
    return operar

somar = criar_operacao('soma')
dividir = criar_operacao('divisao')
print(somar(10, 5))    # 15
print(dividir(10, 2))  # 5.0

3.3. Personalização de comportamento via parâmetros da fábrica

def criar_validador(tipo_validacao, parametro=None):
    def validar(valor):
        if tipo_validacao == 'minimo':
            return valor >= parametro
        elif tipo_validacao == 'maximo':
            return valor <= parametro
        elif tipo_validacao == 'intervalo':
            return parametro[0] <= valor <= parametro[1]
        elif tipo_validacao == 'tamanho':
            return len(str(valor)) <= parametro
        return False
    return validar

validar_idade_minima = criar_validador('minimo', 18)
validar_intervalo = criar_validador('intervalo', (0, 100))

print(validar_idade_minima(25))  # True
print(validar_intervalo(150))    # False

4. Closures como Máquinas de Estado Simples

4.1. Mantendo estado privado sem classes

Closures podem armazenar estado interno sem expor variáveis globalmente, funcionando como máquinas de estado simples.

4.2. Exemplo: closure para média móvel com armazenamento interno

def criar_media_movel():
    valores = []  # Estado privado
    def adicionar_valor(novo_valor):
        valores.append(novo_valor)
        return sum(valores) / len(valores)
    return adicionar_valor

media = criar_media_movel()
print(media(10))  # 10.0
print(media(20))  # 15.0
print(media(30))  # 20.0

4.3. Comparação com abordagem orientada a objetos

# Versão com closure
def criar_contador():
    contagem = 0
    def incrementar():
        nonlocal contagem
        contagem += 1
        return contagem
    return incrementar

contador_closure = criar_contador()

# Versão com classe
class Contador:
    def __init__(self):
        self.contagem = 0
    def incrementar(self):
        self.contagem += 1
        return self.contagem

contador_classe = Contador()

print(contador_closure())  # 1
print(contador_classe.incrementar())  # 1

5. Aplicações Práticas no Dia a Dia

5.1. Lazy evaluation e computação adiada

def criar_calculador_lazy(operacao, a, b):
    def calcular():
        if operacao == 'soma':
            return a + b
        elif operacao == 'potencia':
            return a ** b
        return None
    return calcular

calc_diferido = criar_calculador_lazy('potencia', 2, 10)
# Cálculo só ocorre quando chamado
print(calc_diferido())  # 1024

5.2. Criação de callbacks personalizados

def criar_callback_confirmacao(mensagem, confirmacoes_necessarias=1):
    confirmacoes = 0
    def callback():
        nonlocal confirmacoes
        confirmacoes += 1
        if confirmacoes >= confirmacoes_necessarias:
            return f"Ação confirmada: {mensagem}"
        return f"Confirmação {confirmacoes}/{confirmacoes_necessarias}"
    return callback

confirmar = criar_callback_confirmacao("Excluir arquivo", 2)
print(confirmar())  # Confirmação 1/2
print(confirmar())  # Ação confirmada: Excluir arquivo

5.3. Cache simples com closures (memoization manual)

def memoizar():
    cache = {}
    def funcao_memoizada(n):
        if n not in cache:
            print(f"Calculando fibonacci({n})...")
            if n <= 1:
                cache[n] = n
            else:
                cache[n] = funcao_memoizada(n-1) + funcao_memoizada(n-2)
        return cache[n]
    return funcao_memoizada

fibonacci = memoizar()
print(fibonacci(10))  # Calcula e armazena
print(fibonacci(10))  # Retorna do cache

6. Closures e Escopo de Variáveis

6.1. Regras LEGB e a busca por variáveis não locais

Python busca variáveis na ordem: Local, Enclosing (escopo externo), Global, Built-in. Closures acessam variáveis no escopo "Enclosing".

6.2. A palavra-chave nonlocal e modificação de variáveis em escopo externo

def criar_acumulador():
    total = 0
    def acumular(valor):
        nonlocal total  # Permite modificar variável do escopo externo
        total += valor
        return total
    return acumular

acumulador = criar_acumulador()
print(acumulador(5))   # 5
print(acumulador(10))  # 15
print(acumulador(3))   # 18

6.3. Diferenças entre Python 2 e Python 3 para closures

Em Python 2, closures tinham acesso limitado a variáveis de escopo externo e não existia nonlocal. Python 3 introduziu nonlocal para modificar variáveis em escopos externos não globais.

7. Limitações e Boas Práticas

7.1. Consumo de memória: referências circulares e garbage collection

Closures mantêm referências para variáveis do escopo externo, o que pode impedir a coleta de lixo se não forem usadas adequadamente.

7.2. Quando usar closures vs. classes vs. decoradores

  • Closures: estado simples, poucos métodos, encapsulamento leve
  • Classes: estado complexo, múltiplos métodos, herança necessária
  • Decoradores: modificar comportamento de funções existentes

7.3. Debugging de closures: ferramentas e técnicas

import inspect

def criar_funcao():
    x = 42
    def interna():
        return x
    return interna

func = criar_funcao()
print(inspect.getclosurevars(func))  # Mostra variáveis capturadas

8. Casos de Uso Avançados e Integração

8.1. Closures em combinadores (ex: partial da biblioteca functools)

from functools import partial

def potencia(base, expoente):
    return base ** expoente

quadrado = partial(potencia, expoente=2)
cubo = partial(potencia, expoente=3)

print(quadrado(5))  # 25
print(cubo(5))      # 125

8.2. Integração com decoradores: closures como base dos decoradores

def decorador_tempo(funcao):
    import time
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = funcao(*args, **kwargs)
        fim = time.time()
        print(f"{funcao.__name__} executou em {fim - inicio:.4f} segundos")
        return resultado
    return wrapper

@decorador_tempo
def operacao_demorada():
    return sum(range(1000000))

operacao_demorada()

8.3. Exemplo completo: sistema de plugins com funções de fábrica

class PluginManager:
    def __init__(self):
        self._plugins = {}

    def registrar_plugin(self, nome, fabrica):
        self._plugins[nome] = fabrica

    def executar_plugin(self, nome, *args, **kwargs):
        if nome in self._plugins:
            plugin = self._plugins[nome](*args, **kwargs)
            return plugin()
        raise ValueError(f"Plugin {nome} não encontrado")

# Fábrica de plugins
def fabrica_saudacao(idioma):
    def saudar():
        saudos = {
            'pt': "Olá, mundo!",
            'en': "Hello, world!",
            'es': "¡Hola, mundo!"
        }
        return saudos.get(idioma, "Idioma não suportado")
    return saudar

manager = PluginManager()
manager.registrar_plugin('saudacao_pt', lambda: fabrica_saudacao('pt'))
manager.registrar_plugin('saudacao_en', lambda: fabrica_saudacao('en'))

print(manager.executar_plugin('saudacao_pt'))  # Olá, mundo!
print(manager.executar_plugin('saudacao_en'))  # Hello, world!

Referências