Trilha de aprendizado · Nível 9 · Tutorial 4

Controlar a vinculação tardia em closures

Ao concluir, você será capaz de diagnosticar capturas inesperadas em funções criadas em laços e escolher quando compartilhar uma variável ou fixar a referência ao valor corrente.

  • Nível: Intermediário
  • Duração: 15 min
  • 7 passos
Controlar a vinculação tardia em closures

O que você vai percorrer

  1. Prever o resultado de chamadas adiadas Diferencie a criação de closures de sua execução e preveja o valor consultado quando elas são chamadas após o laço. 2 min
  2. Entender por que funções distintas compartilham a variável Veja por que várias funções criadas no mesmo laço podem consultar o mesmo vínculo — seja com lambda ou com def. 2 min
  3. Fixar a referência com um parâmetro padrão Use um parâmetro padrão para associar cada função ao valor corrente da iteração, sem consultar depois a variável compartilhada do laço. 2 min
  4. Separar as capturas com uma fábrica Use uma fábrica de funções para criar um vínculo próprio para cada função produzida no laço. 2 min
  5. Distinguir referência fixada de objeto copiado Veja o que uma closure conserva ao usar parâmetro padrão ou fábrica: uma referência ao objeto, não uma cópia dele. 2 min
  6. Escolher quando compartilhar e quando fixar Escolha a estratégia de captura a partir do comportamento esperado e da interface que cada função deve oferecer. 2 min
  7. Corrigir e verificar um conjunto de closures Aplique o diagnóstico de vinculação tardia em um script completo, corrija as closures e confirme o efeito de mutações e reatribuições. 3 min

O que você vai aprender

  • Prever os resultados de closures chamadas depois do término de um laço.
  • Explicar por que funções distintas podem consultar a mesma variável capturada.
  • Corrigir capturas indesejadas usando parâmetros padrão ou uma fábrica de funções.
  • Reconhecer que fixar uma referência não cria uma cópia independente de um objeto mutável.

Antes de começar

  • Criar closures com estado e nonlocal
  • Escolher entre lambda, funções nomeadas e compreensões
  • Usar argumentos nomeados e valores padrão
  • Percorrer textos e intervalos com for e range

Passo 1 de 7

Prever o resultado de chamadas adiadas

Diferencie a criação de closures de sua execução e preveja o valor consultado quando elas são chamadas após o laço.

Criar não é executar

Funções guardadas para depois

Uma função interna pode ser criada em um laço e armazenada em uma lista sem que seu corpo seja executado naquele instante. A execução só acontece quando você chama cada função, por exemplo com funcao().

Em uma closure, o nome vindo da função externa é consultado quando a função interna roda. Portanto, não suponha que a criação automática guardou o valor que o laço tinha naquela iteração.

Três funções, chamadas depois

Leia primeiro sem executar: em que momento numero é consultado?

python
def criar_consultas():
    consultas = []

    for numero in range(3):
        consultas.append(lambda: numero)

    return consultas

consultas = criar_consultas()
print([consulta() for consulta in consultas])

A linha do tempo da chamada

O valor relevante é o da execução

Durante o laço, são criadas e adicionadas à lista três funções internas. O corpo de cada lambda ainda não foi executado.

Depois da última iteração, numero vale 2. A função externa retorna a lista; só então a compreensão chama cada consulta(). Em cada chamada, a consulta a numero encontra o valor vigente: 2.

Criação e chamadas em momentos distintos

A imagem separa o momento de montar a lista do momento de executar as funções.

Diagrama de linha do tempo mostrando três funções lambda criadas durante iterações com valores zero, um e dois, guardadas em uma lista, seguido do fim do laço com número igual a dois e três chamadas posteriores retornando dois.

As funções são criadas no laço, mas a consulta ao nome acontece somente nas chamadas posteriores.

Faça a previsão

Qual é a saída?

Qual lista será impressa pelo código apresentado?

Passo 2 de 7

Entender por que funções distintas compartilham a variável

Veja por que várias funções criadas no mesmo laço podem consultar o mesmo vínculo — seja com lambda ou com def.

Um laço, um vínculo reutilizado

A variável do for não se multiplica

Dentro de uma chamada de função, o for atualiza o mesmo vínculo da variável do laço a cada iteração. Ele não cria um novo escopo de função para cada volta.

Por isso, funções internas diferentes criadas nesse laço podem apontar para o mesmo vínculo de numero. Quando forem chamadas depois, todas consultarão o valor que esse vínculo tiver naquele momento.

Funções diferentes, vínculo compartilhado

Cada cartão representa uma função distinta. As setas convergem porque todas acessam a mesma variável do laço.

Diagrama com três funções distintas apontando para um único vínculo de variável de laço, cujo valor muda de 1 para 2 e depois 3.

O for atualiza um único vínculo; criar uma função não copia automaticamente o valor atual.

Lambda e def seguem a mesma regra

Duas formas, a mesma captura

As duas listas são criadas em uma única chamada de montar().

python
def montar():
    com_lambda = []
    com_def = []

    for numero in range(3):
        com_lambda.append(lambda: numero)

        def consultar():
            return numero

        com_def.append(consultar)

    return com_lambda, com_def

lambdas, funcoes = montar()

print([f() for f in lambdas])  # [2, 2, 2]
print([f() for f in funcoes])  # [2, 2, 2]

Trocar a sintaxe não troca o vínculo

lambda e def criam objetos função distintos, mas ambos consultam o mesmo numero do ambiente envolvente. Assim, substituir lambda por def não resolve uma captura compartilhada.

Durante ou depois do laço?

O momento da chamada muda o resultado

Aqui, cada função é chamada imediatamente e depois do término do laço.

python
def observar():
    funcoes = []

    for numero in range(3):
        def consultar():
            return numero

        funcoes.append(consultar)
        print("durante:", consultar())

    print("depois:", [funcao() for funcao in funcoes])

observar()
# durante: 0
# durante: 1
# durante: 2
# depois: [2, 2, 2]

Resultados diferentes não provam capturas separadas

Durante cada iteração, o vínculo compartilhado ainda tem o valor corrente, então a chamada imediata observa 0, depois 1, depois 2. Ao final, o laço deixou esse mesmo vínculo com 2; as chamadas posteriores consultam todas esse valor.

Verifique sua explicação

Verdadeiro ou falso

Se três funções internas foram criadas no mesmo laço, elas necessariamente possuem três vínculos independentes para a variável do laço.

Passo 3 de 7

Fixar a referência com um parâmetro padrão

Use um parâmetro padrão para associar cada função ao valor corrente da iteração, sem consultar depois a variável compartilhada do laço.

Guardar o valor na criação

O padrão é avaliado ao criar a função

Em uma função criada no laço, escreva um parâmetro padrão com o valor corrente: valor=i. Esse valor é associado ao objeto função naquela iteração. No corpo, use o parâmetro local valor, e não a variável livre i.

Duas estratégias de captura

À esquerda, todas as funções consultam o mesmo vínculo i. À direita, cada função possui seu próprio parâmetro padrão valor.

Diagrama comparando três funções apontando para uma única variável de laço i com valor final 2 e três funções com parâmetros padrão separados contendo 0, 1 e 2.

Usar valor=i cria uma associação por função no momento de sua criação.

Dica

Leia como uma associação

A expressão valor=i não muda a regra da variável do laço. Ela apenas faz cada função receber, na criação, um valor padrão próprio para o parâmetro local valor.

Com def ou lambda

Duas formas equivalentes de fixar a referência

Nas duas listas, as chamadas ocorrem depois do término do laço.

python
def criar_com_def():
    funcoes = []
    for i in range(3):
        def consultar(valor=i):
            return valor
        funcoes.append(consultar)
    return funcoes


def criar_com_lambda():
    funcoes = []
    for i in range(3):
        funcoes.append(lambda valor=i: valor)
    return funcoes

print([funcao() for funcao in criar_com_def()])     # [0, 1, 2]
print([funcao() for funcao in criar_com_lambda()])  # [0, 1, 2]

O que mudou?

Em consultar(valor=i) e lambda valor=i: valor, o corpo retorna valor, que é local à chamada. Como nenhum argumento foi fornecido, Python usa o padrão armazenado em cada função.

Argumento explícito substitui o padrão

O padrão não bloqueia novos argumentos

O valor fixado é o padrão da chamada, não uma constante inacessível.

python
funcoes = criar_com_def()

print(funcoes[0]())       # 0: usa o padrão guardado
print(funcoes[0](99))     # 99: argumento explícito substitui o padrão nesta chamada
print(funcoes[0]())       # 0: o padrão da função continua sendo 0

Dica

Atenção à interface

Essa solução adiciona um parâmetro que pode ser informado por quem chama a função. Se a função resultante não puder aceitar esse argumento adicional, há outra abordagem para ver no próximo step.

Pratique a correção

Complete o parâmetro padrão

Complete apenas o conteúdo entre parênteses para que as chamadas sem argumentos retornem [2, 4, 6] após o laço:

<code>funcoes = []
for numero in [2, 4, 6]:
funcoes.append(lambda ( ____ ): fixado)

print([funcao() for funcao in funcoes])</code>

Preveja uma chamada com argumento

Após a correção anterior, qual é o resultado de funcoes[1](40)?

Passo 4 de 7

Separar as capturas com uma fábrica

Use uma fábrica de funções para criar um vínculo próprio para cada função produzida no laço.

Uma chamada, um ambiente próprio

Passe o valor para a fábrica

Quando o laço chama uma fábrica, o valor corrente é recebido como argumento. Cada chamada da fábrica cria seu próprio vínculo para esse parâmetro; a função retornada consulta esse vínculo, não a variável reutilizada pelo laço.

Vínculos separados por chamada

Cada seta representa uma chamada distinta da fábrica.

Diagrama mostrando os valores 2, 4 e 6 entrando separadamente em três chamadas de uma fábrica, que retorna três funções conectadas a vínculos independentes contendo 2, 4 e 6.

Três chamadas da fábrica criam três ambientes: cada função retornada acessa um vínculo diferente.

A fábrica dentro do laço

Crie cada consulta por meio da fábrica

A função retornada não recebe argumentos: sua interface permanece sem parâmetros.

python
def criar_consulta(valor):
    def consultar():
        return valor
    return consultar

consultas = []
for numero in [2, 4, 6]:
    consultas.append(criar_consulta(numero))

print([consulta() for consulta in consultas])  # [2, 4, 6]

O detalhe decisivo

criar_consulta(numero) é executada em cada iteração. Assim, as três funções consultar foram criadas em chamadas externas diferentes. Em contraste, definir várias funções diretamente no mesmo corpo externo faria todas consultarem o mesmo vínculo do laço.

Dica

Interface preservada

Diferentemente de uma solução com parâmetro padrão exposto, consultar continua sendo chamada simplesmente como consulta(). O parâmetro valor existe apenas na fábrica.

Aplique a separação

Reescreva usando a fábrica

Considere o código abaixo, que deve produzir três funções sem argumentos, uma para cada cor:

mensagens = []
for cor in ["azul", "verde", "vermelho"]:
    # complete aqui

Escreva uma fábrica curta e a linha que deve entrar no laço. Em seguida, explique por que as funções retornadas não consultam o mesmo vínculo de cor.

Escreva pelo menos 80 caracteres (0/80).

Passo 5 de 7

Distinguir referência fixada de objeto copiado

Veja o que uma closure conserva ao usar parâmetro padrão ou fábrica: uma referência ao objeto, não uma cópia dele.

Referência fixada não é cópia

O que fica guardado

Ao escrever lambda registro=registro: registro, o parâmetro padrão guarda a referência atual ao objeto. Ele não cria uma nova lista nem um novo dicionário.

Por isso, se esse mesmo objeto for alterado depois, a função verá a alteração.

Mutar ou reatribuir?

Diagrama em dois painéis: em um, uma closure e uma variável externa apontam para a mesma lista, que recebe um novo item; no outro, a variável externa passa a apontar para outra lista, enquanto a closure continua apontando para a lista original.

Mutação altera o objeto já referenciado; reatribuição troca apenas a associação da variável externa.

O mesmo objeto continua visível

Mutação após criar a função

Execute e observe os dois resultados.

python
def criar_consulta(registro):
    return lambda registro=registro: registro

pedido = {"status": "aberto"}
consultar = criar_consulta(pedido)

pedido["status"] = "enviado"  # muta o mesmo dicionário
print(consultar())               # {'status': 'enviado'}

pedido = {"status": "cancelado"}  # associa o nome a outro dicionário
print(consultar())                  # {'status': 'enviado'}

Atenção

Duas operações diferentes

pedido["status"] = "enviado" modifica o dicionário original. Já pedido = {...} faz o nome pedido apontar para outro dicionário. A closure mantém a referência que já havia fixado.

Teste sua previsão

Mutação compartilhada

Considere:

def criar_leitor(itens):
    return lambda itens=itens: itens

nomes = ["Ana"]
leitor = criar_leitor(nomes)
nomes.append("Beto")

A chamada leitor() retorna ["Ana", "Beto"].

Fábrica também não copia

Vínculos separados podem apontar para um único objeto

Uma fábrica cria um vínculo próprio em cada chamada, mas os argumentos recebidos ainda podem ser referências ao mesmo objeto. Portanto, vínculos independentes não implicam objetos independentes.

Duas closures, uma lista

As duas chamadas da fábrica recebem a mesma lista.

python
def criar_leitor(valor):
    def ler():
        return valor
    return ler

etiquetas = ["novo"]
ler_a = criar_leitor(etiquetas)
ler_b = criar_leitor(etiquetas)

etiquetas.append("urgente")
print(ler_a())  # ['novo', 'urgente']
print(ler_b())  # ['novo', 'urgente']

Dois vínculos, um objeto

No código anterior, ler_a e ler_b têm vínculos capturados independentes, mas ambos observam a alteração em etiquetas.

Passo 6 de 7

Escolher quando compartilhar e quando fixar

Escolha a estratégia de captura a partir do comportamento esperado e da interface que cada função deve oferecer.

Comece pelo comportamento desejado

Vinculação tardia também pode ser uma escolha

A vinculação tardia não é necessariamente um defeito. Mantenha o vínculo compartilhado quando todas as funções devem consultar o valor mais atual da variável externa no momento da chamada.

Fixe a referência quando cada função deve manter a associação que recebeu ao ser criada. Se a exigência for que cada função tenha também um objeto mutável independente, fixar a referência não basta: será preciso criar uma cópia adequada do objeto.

Três necessidades diferentes

Compare o que cada estratégia preserva.

Diagrama com três conjuntos de closures: no primeiro, várias funções apontam para um único vínculo que muda; no segundo, cada função aponta para uma referência própria; no terceiro, cada função aponta para um objeto independente.

Compartilhar um vínculo, conservar referências distintas e ter objetos independentes são requisitos diferentes.

A interface ajuda a decidir

Exemplo

Parâmetro padrão ou fábrica?

Use parâmetro padrão quando deixar um parâmetro opcional explícito for aceitável e claro:

leitores = [lambda item=item: item for item in itens]

Cada leitor pode ser chamado sem argumento, mas leitores[0](outro_item) substitui o padrão naquela chamada.

Prefira uma fábrica quando a função resultante deve expor somente a interface desejada, sem esse parâmetro opcional:

def criar_leitor(item):
    def ler():
        return item
    return ler

leitores = [criar_leitor(item) for item in itens]

Nos dois casos, a referência corrente é conservada. Nenhum dos dois cria uma cópia automática de item.

Dica

Quadro de decisão

  • A função deve acompanhar uma futura reatribuição do vínculo externo? Mantenha a vinculação tardia.
  • Cada função deve lembrar a associação da própria criação? Use parâmetro padrão ou fábrica.
  • A interface deve permanecer sem parâmetro adicional? Use uma fábrica.
  • Cada função precisa de dados mutáveis isolados? Crie cópias independentes; apenas separar vínculos não resolve.

Associe requisito e estratégia

Decisão de captura

Relacione cada requisito à estratégia mais adequada.

Toque em um item e depois no par correspondente.

Passo 7 de 7

Corrigir e verificar um conjunto de closures

Aplique o diagnóstico de vinculação tardia em um script completo, corrija as closures e confirme o efeito de mutações e reatribuições.

Diagnóstico: associações esperadas

O contrato

Uma função de consulta deve ser criada para cada registro e chamada apenas depois que criar_consultas terminar. Cada consulta precisa conservar a referência ao registro correspondente à sua criação.

No código inicial, todas as lambdas consultam a mesma variável registro do laço quando são chamadas. Portanto, elas acabam retornando o último registro.

Compartilhamento acidental x contrato

Compare as setas antes de alterar o código.

Diagrama comparando três funções apontando para um único último registro no caso incorreto e três funções apontando cada uma para seu respectivo registro no caso correto.

À esquerda, a consulta tardia usa um vínculo compartilhado. À direita, cada função conserva a referência do momento em que foi criada.

Prática local: corrija e execute

Seu desafio

No seu computador, crie um arquivo chamado consultas.py, copie o script abaixo e execute python consultas.py.

Primeiro, rode sem alterar nada: o primeiro assert deve falhar. Em seguida, corrija somente a linha marcada com # CORRIJA AQUI, usando uma das estratégias estudadas. Execute de novo até que não haja saída nem erro — isso indica que todos os asserts passaram.

consultas.py

Script inicial com uma captura tardia intencional.

python
def criar_consultas(registros):
    consultas = []

    for registro in registros:
        # CORRIJA AQUI: cada consulta deve conservar este registro.
        consultas.append(lambda: registro)

    return consultas


registros = [
    {"id": "A1", "status": "novo"},
    {"id": "B2", "status": "em análise"},
    {"id": "C3", "status": "concluído"},
]

consultas = criar_consultas(registros)

# Cada função deve retornar o objeto que correspondia à sua iteração.
assert consultas[0]() is registros[0]
assert consultas[1]() is registros[1]
assert consultas[2]() is registros[2]

# A mutação do mesmo objeto deve continuar visível na consulta que o referencia.
registros[0]["status"] = "revisado"
assert consultas[0]()["status"] == "revisado"

# Trocar o objeto na lista não redireciona a referência antes conservada.
registros[0] = {"id": "A1", "status": "substituído"}
assert consultas[0]()["status"] == "revisado"
assert consultas[0]() is not registros[0]

print("Verificações concluídas.")

Dica

Uma correção possível

Para usar parâmetro padrão, substitua a linha marcada por:

consultas.append(lambda registro=registro: registro)

O parâmetro padrão é avaliado ao criar cada lambda. A função passa a usar seu parâmetro local registro, e não consulta a variável livre do laço. Você também pode resolver com uma fábrica de funções, se preferir manter a função retornada sem esse parâmetro opcional.

Verifique o que cada assert demonstrou

Leia os resultados pelo contrato

Os três primeiros asserts verificam identidades: cada consulta retorna exatamente o objeto da respectiva posição original.

Depois, a alteração de status é uma mutação no mesmo dicionário; por isso ela aparece na primeira consulta. Já a atribuição de um novo dicionário a registros[0] troca o objeto guardado na lista, mas não altera a referência que a closure já conservava.

Relate sua verificação

Qual estratégia você usou para corrigir a captura? Explique o que ocorreu após a mutação do primeiro registro e após a substituição de registros[0] por outro dicionário.

Escreva pelo menos 120 caracteres (0/120).

Síntese final

Resumo

Checklist para closures criadas em laços

  • Pergunte quando o corpo da função interna será executado: criar e armazenar uma função não a executa.
  • Se várias funções foram criadas no mesmo laço e consultam a variável do laço depois, elas podem compartilhar o mesmo vínculo final.
  • Use um parâmetro padrão ou uma fábrica quando cada função deve conservar a referência corrente.
  • Uma referência conservada acompanha mutações do mesmo objeto, mas não é redirecionada quando a variável externa passa a apontar para outro objeto.
  • Compartilhar um vínculo pode ser intencional; a escolha depende do comportamento desejado.

Tutorial concluído

Parabéns! Você concluiu: Controlar a vinculação tardia em closures

Você concluiu a prática de vinculação tardia em closures. Agora consegue identificar o vínculo consultado, fixar a referência necessária e distinguir referência preservada de cópia de objeto.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso