Closures, Protocolos & Avaliação Preguiçosa
Iteradores, geradores, decoradores e gerenciadores de contexto parecem quatro recursos diferentes, mas compartilham duas ideias subjacentes: protocolos (objetos que implementam um pequeno conjunto acordado de dunder methods) e closures (funções que carregam estado de um escopo envolvente entre chamadas).
for delega ao protocolo de iterador, with delega ao protocolo de gerenciador de contexto, e @decorator é apenas um closure envolvendo uma função - nenhum desses é um caso especial da linguagem, são todos objetos comuns que por acaso implementam os ganchos corretos.
Esta página é o modelo por trás de Iteradores & o Protocolo de Iterador, Geradores & yield, Gerenciadores de Contexto, e Escrevendo Decoradores; uma vez que isso se torne claro, itertools e functools serão lidos como uma caixa de ferramentas construída sobre essas mesmas duas ideias, em vez de um saco de utilitários não relacionados.
Resumo
- A sintaxe
for,withe@decoratordo Python são todas açúcar sintático sobre protocolos - objetos que implementam dunder methods específicos - e closures são como esses objetos (ou funções comuns) carregam estado entre chamadas. - Por que Importa: Uma vez que protocolos e closures são visíveis como o mecanismo compartilhado, geradores, decoradores e gerenciadores de contexto deixam de ser tópicos separados para memorizar e se tornam três aplicações de uma ideia.
- Conceitos Chave: protocolo de iterador, avaliação preguiçosa, closure / variável livre, protocolo de gerenciador de contexto, função de ordem superior.
- Quando Usar: Construir um iterável customizado, escrever um decorador que precisa lembrar estado entre chamadas, ou decidir entre um gerador e uma lista ao processar dados grandes ou infinitos.
- Limitações / Trade-offs: Objetos preguiçosos baseados em protocolo (geradores, iteradores) trocam acesso aleatório e reutilização por eficiência de memória - a maioria é de passagem única e não pode ser indexada ou reiniciada.
- Tópicos Relacionados: o modelo de objeto e dispatch dunder, escopo de função (LEGB), iteração assíncrona, memoização.
Fundamentos
Um iterador é qualquer objeto com um método __next__ que retorna o próximo valor ou levanta StopIteration quando esgotado; um iterável é qualquer objeto com um método __iter__ que retorna tal iterador.
for item in obj: é inteiramente açúcar sintático sobre isso: Python chama iter(obj) uma vez para obter um iterador, então chama next() nele repetidamente até que StopIteration pare o loop - não há outra mágica envolvida.
Uma função geradora (qualquer função contendo yield) é a maneira mais fácil de obter um iterador sem escrever uma classe: chamá-la não executa o corpo da função de forma alguma, ela imediatamente retorna um objeto gerador, e o corpo só executa até o próximo yield cada vez que next() é chamado nele.
Essa execução adiada é avaliação preguiçosa: valores são produzidos um de cada vez, sob demanda, em vez de todos de uma vez - é por isso que um gerador pode representar uma sequência infinita sem nunca ficar sem memória.
Um closure é uma função aninhada que referencia uma variável do escopo de uma função externa; Python mantém essa variável viva em um objeto célula compartilhado enquanto o closure existir, que é como um decorador ou uma função semelhante a um gerador pode lembrar estado entre chamadas separadas sem uma classe.
Mecânicas & Interações
A mecânica que mais confunde as pessoas é que closures capturam uma referência à célula da variável, não um instantâneo de seu valor no momento da definição - é por isso que um loop que cria vários closures sobre uma variável de loop muitas vezes surpreende as pessoas, pois todas elas veem o valor final do loop.
def make_multiplier(factor):
def multiply(x):
return x * factor # 'factor' é lido da célula envolvente, ao vivo
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5with obj: é açúcar sintático sobre o protocolo de gerenciador de contexto: Python chama obj.__enter__() antes do bloco e garante que obj.__exit__(exc_type, exc, tb) seja executado após ele, mesmo que o bloco tenha levantado uma exceção - é exatamente por isso que with é a maneira idiomática de garantir a limpeza (fechar um arquivo, liberar um lock) em vez de um try/finally manual.
@contextlib.contextmanager constrói um gerenciador de contexto a partir de uma função geradora em vez de uma classe: o código antes do único yield se torna o comportamento de __enter__, o código depois dele se torna o comportamento de __exit__, e qualquer exceção de dentro do bloco with é levantada naquele ponto de yield dentro do gerador, permitindo que você a trate com um try/except comum em torno do yield.
Um decorador não é nada mais do que uma função de ordem superior: @log_calls acima de uma definição de função é exatamente equivalente a escrever my_func = log_calls(my_func) imediatamente após defini-la, e a função wrapper que ela retorna é um closure que se lembra da função original para poder chamá-la mais tarde, tipicamente após ou em torno de algum comportamento extra.
yield from delega a iteração para um sub-gerador ou qualquer iterável, encaminhando seus valores (e, menos comumente, valores enviados e exceções) como se o gerador externo os tivesse produzido. Este é o mecanismo que Yield From & Sub-Generators cobre em profundidade, e existe especificamente para que geradores possam ser compostos sem o boilerplate manual de for ... yield.
Considerações Avançadas e Aplicações
itertools e functools não introduzem novos mecanismos - são funções compostas construídas inteiramente sobre o protocolo de iterador e closures já descritos aqui: itertools.chain retorna um iterador que percorre preguiçosamente vários iteráveis em sequência, e functools.lru_cache envolve uma função em um closure que lembra argumentos e resultados anteriores.
O maior trade-off arquitetural neste espaço é a avaliação preguiçosa versus ansiosa: uma list comprehension calcula cada elemento imediatamente e os mantém todos na memória, enquanto uma generator expression ou pipeline itertools calcula cada elemento apenas quando solicitado, ao custo de ser de passagem única e não indexável.
itertools.batched (adicionado no Python 3.12, com um argumento de palavra-chave strict adicionado no 3.13) é um exemplo recente dessa mesma filosofia de composição preguiçosa: ele transforma qualquer iterável em um iterador de tuplas de tamanho fixo sem nunca materializar toda a entrada na memória.
Código assíncrono estende exatamente o mesmo conceito de protocolo para um modelo de execução diferente: async for usa __aiter__/__anext__ em vez de __iter__/__next__, e async with usa __aenter__/__aexit__ - o padrão do protocolo é idêntico, apenas os métodos são corrotinas que podem await dentro deles.
| Abordagem | Força | Fraqueza | Melhor ajuste |
|---|---|---|---|
| List comprehension | Simples, indexável, reutilizável várias vezes | Materializa tudo na memória imediatamente | Dados pequenos a médios que você iterará mais de uma vez |
| Generator expression / function | Memória constante independentemente do comprimento; suporta sequências infinitas | Apenas passagem única; sem indexação ou len() | Dados grandes ou ilimitados, pipelines de streaming/ETL |
| Iterador baseado em classe customizada | Controle total sobre o estado, suporta métodos extras além da iteração | Mais boilerplate do que um gerador para o mesmo comportamento | Iteráveis reutilizáveis que precisam ser reiniciados ou expor API extra |
Composição itertools | Preguiçosa, eficiente em memória, evita bookkeeping manual de loop | Lê menos linearmente para desenvolvedores não familiarizados com o módulo | Combinatória, encadeamento, fatiamento de iteradores sem uma passagem completa |
Decoradores se compõem da mesma forma em escala: empilhar múltiplos decoradores (@a e depois @b acima de uma função) os aplica de baixo para cima, então func = a(b(func)), e perder o controle dessa ordem - ou esquecer functools.wraps para preservar o __name__/__doc__ da função original - são as duas fontes mais comuns de bugs confusos de decoradores em bases de código reais.
Concepções Erradas Comuns
- "Um gerador é uma lista computada preguiçosamente." É mais próximo de um iterador de uso único: você não pode indexá-lo, verificar seu comprimento ou reiniciá-lo depois de esgotado, ao contrário de uma lista que por acaso é computada preguiçosamente.
- "
fortem suporte especial embutido para listas, dicionários e arquivos."forsempre chamaiter()e depoisnext()repetidamente - todo iterável, embutido ou customizado, passa pelo protocolo idêntico. - "Closures copiam o valor da variável envolvente quando a função interna é definida." Eles capturam uma referência viva à célula da variável, é por isso que closures criados em um loop frequentemente veem o valor final dessa variável, a menos que seja explicitamente capturado por iteração.
- "Um decorador muda o código fonte real da função." Ele substitui o nome por um novo objeto de função (geralmente um wrapper); a função original ainda existe, apenas referenciada de dentro do closure do wrapper.
- "
withapenas executa algum código antes e depois de um bloco, como duas chamadas de função." Ele garante especificamente que__exit__seja executado mesmo quando o bloco levanta uma exceção, e__exit__pode inspecionar ou até mesmo suprimir essa exceção - um par simples de chamadas "antes e depois" não pode fazer nenhum dos dois.
FAQs
O que é o protocolo de iterador, concretamente?
Dois dunder methods: __iter__, que retorna um iterador, e __next__ nesse iterador, que retorna o próximo valor ou levanta StopIteration quando não há mais nada - loops for são construídos inteiramente em chamar esses dois métodos.
Chamar uma função geradora executa seu código imediatamente?
Não - chamá-la retorna um objeto gerador sem executar nenhuma parte do corpo da função; o corpo só executa até o próximo yield cada vez que next() é chamado nesse objeto.
O que significa "avaliação preguiçosa" neste contexto?
Valores são produzidos um de cada vez, apenas quando solicitados, em vez de todos serem computados e armazenados antecipadamente - isso é o que permite que um gerador represente até mesmo uma sequência infinita sem esgotar a memória.
O que exatamente um closure captura - o valor ou a variável?
Ele captura uma referência à célula da variável envolvente, não um instantâneo de seu valor naquele momento, é por isso que o valor atual da variável é o que um closure vê cada vez que ele executa, não o que quer que tenha sido quando o closure foi criado.
Por que closures feitos dentro de um loop às vezes se comportam todos da mesma maneira, inesperadamente?
Porque todos eles compartilham a mesma célula da variável envolvente em vez de cada um receber sua própria cópia, então quando qualquer um deles realmente executa, todos eles leem o valor que essa variável acabou segurando após o término do loop.
O que `with obj:` garante realmente?
Que obj.__exit__() seja executado após o bloco ser concluído, mesmo que o bloco tenha levantado uma exceção - obj.__enter__() é executado primeiro e seu valor de retorno é o que as name vincula, se usado.
O `@contextlib.contextmanager` é um atalho ou um mecanismo totalmente diferente?
É um atalho sobre o mesmo protocolo: o código antes do único yield do gerador se torna o comportamento de __enter__, e o código depois dele se torna o comportamento de __exit__, com exceções do bloco with aparecendo naquele ponto de yield.
O que é um decorador, mecanicamente, em uma frase?
@decorator acima de uma definição de função é exatamente equivalente a escrever func = decorator(func) logo após defini-la - nada mais misterioso do que uma chamada de função de ordem superior.
Por que `functools.wraps` importa ao escrever um decorador?
Sem ele, a função wrapper substitui o __name__, __doc__ e outros metadados originais pelos seus próprios valores genéricos, o que quebra a introspecção, a saída de depuração e as ferramentas de documentação que esperam ver a identidade da função original.
São `itertools` e `functools` um sistema separado de iteradores e closures?
Não - funções itertools retornam iteradores preguiçosos construídos sobre o mesmo protocolo de iterador, e ferramentas functools como lru_cache ou partial são closures envolvendo chamáveis, então ambos os módulos são composições dos dois mecanismos que esta página descreve.
Quando devo usar um gerador em vez de uma list comprehension?
Quando os dados são grandes, ilimitados ou precisam apenas de uma única passagem - um gerador mantém apenas seu estado atual na memória, enquanto uma list comprehension materializa cada elemento imediatamente, o que custa mais memória, mas permite reutilização e indexação.
Como `async for` se relaciona com o protocolo de iterador regular?
É o mesmo padrão de protocolo aplicado a corrotinas: __aiter__/__anext__ substituem __iter__/__next__, e ambos os métodos podem await internamente, permitindo que a iteração pause em I/O real em vez de apenas em computação síncrona.
Relacionados
- Iteradores & o Protocolo de Iterador -
__iter__/__next__e iteráveis customizados em profundidade - Geradores & yield - estado do gerador e pipelines preguiçosos de perto
- Gerenciadores de Contexto - o protocolo
__enter__/__exit__econtextlib - Escrevendo Decoradores - closures envolvendo chamáveis, tratamento de argumentos e empilhamento
- Closures & Captura de Escopo - a mecânica de captura de célula em detalhes completos
- itertools - composição preguiçosa construída sobre o protocolo de iterador
Versões da Stack: Esta página foi escrita para Python 3.14 (estável) e Python 3.13 (manutenção), incluindo
itertools.batched(adicionado em 3.12,strict=adicionado em 3.13).