Trilha de aprendizado · Nível 9 · Tutorial 3

Criar closures com estado e nonlocal

Ao concluir, você será capaz de retornar funções que conservam acesso ao ambiente de criação e mantêm estado local sem depender de variáveis globais.

  • Nível: Intermediário
  • Duração: 22 min
  • 7 passos
Criar closures com estado e nonlocal

O que você vai percorrer

  1. Definir e retornar uma função interna Crie uma fábrica simples de multiplicadores e separe o momento de obter uma função do momento de executá-la. 2 min
  2. Entender o que a closure conserva Veja por que uma função retornada continua encontrando variáveis do ambiente em que foi criada. 3 min
  3. Atualizar estado com nonlocal Crie um contador que preserva seu estado entre chamadas e use nonlocal para atualizar o vínculo capturado. 4 min
  4. Criar estados independentes Veja como cada chamada de uma fábrica cria uma closure com seu próprio estado e como isso difere de apenas dar outro nome à mesma função. 3 min
  5. Distinguir mutação de reatribuição Saiba identificar quando uma closure modifica a lista capturada e quando troca o vínculo para outra lista. 3 min
  6. Compartilhar estado entre funções internas Crie um par de funções que consulta e atualiza o mesmo estado local de uma única fábrica. 3 min
  7. Aplicar e revisar: um registrador com estado Integre closures, nonlocal e mutação em uma fábrica de registradores com estado privado e independente. 5 min

O que você vai aprender

  • Definir e retornar uma função interna que utilize variáveis da função externa.
  • Explicar por que uma closure continua acessando variáveis após o retorno da função externa.
  • Usar nonlocal para reatribuir uma variável de um escopo envolvente.
  • Criar closures com estados independentes e distinguir reatribuição de mutação do objeto capturado.

Antes de começar

  • Passar funções como argumentos
  • Controlar o escopo e o estado das funções
  • Controlar referências e cópias de coleções

Passo 1 de 7

Definir e retornar uma função interna

Crie uma fábrica simples de multiplicadores e separe o momento de obter uma função do momento de executá-la.

Uma função pode criar outra

Função interna

Uma função pode ser definida dentro do corpo de outra. A função externa pode então devolver a função interna como valor.

Nesta fábrica, criar_multiplicador(3) não faz uma multiplicação ainda: ela produz uma função configurada para multiplicar por 3.

Fábrica de multiplicadores

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

    return multiplicar

triplicar = criar_multiplicador(3)

Retornar não é chamar

Dois momentos distintos

Diagrama em duas etapas: a chamada criar_multiplicador com 3 cria e retorna a função multiplicar; depois a chamada triplicar com 5 executa multiplicar e produz 15.

Primeiro obtenha a função; depois chame a função obtida.

Os parênteses fazem diferença

Use return multiplicar, sem parênteses, para devolver a própria função.

Depois, triplicar(5) chama a função guardada em triplicar e retorna 15. Se você escrevesse return multiplicar(...), estaria chamando a função interna naquele ponto — e precisaria fornecer o argumento numero.

Exemplo

Chamada posterior

resultado = triplicar(5)
print(resultado)  # 15

criar_multiplicador(3) cria a função e a associa a triplicar. Só triplicar(5) executa o corpo de multiplicar.

Organize a sequência

Da criação ao resultado

Coloque os acontecimentos na ordem em que ocorrem neste código:

triplicar = criar_multiplicador(3)
resultado = triplicar(5)
  1. `triplicar(5)` chama a função interna.
  2. A função interna `multiplicar` é definida e retornada, sendo associada a `triplicar`.
  3. O corpo de `multiplicar` calcula e retorna `15`.
  4. `criar_multiplicador(3)` é chamada.

Passo 2 de 7

Entender o que a closure conserva

Veja por que uma função retornada continua encontrando variáveis do ambiente em que foi criada.

O ambiente de criação acompanha a função

Closure: função + acesso ao ambiente

Quando uma função interna usa um nome definido na função externa e é retornada, ela forma uma closure: uma função que conserva acesso aos vínculos do ambiente envolvente em que foi criada.

Esse escopo envolvente é definido pelo aninhamento dos def, não pelo lugar onde você chama a função retornada.

Uma saudação configurada

python
def criar_saudacao(nome):
    def saudar():
        return f"Olá, {nome}!"

    return saudar

para_ana = criar_saudacao("Ana")
print(para_ana())  # Olá, Ana!

De onde vem `nome`?

Diagrama mostrando criar_saudacao com o vínculo nome para Ana e a função saudar retornada mantendo acesso a esse vínculo depois que a chamada externa termina.

saudar foi criada dentro de criar_saudacao; por isso, encontra o vínculo de nome naquele ambiente.

Como Python procura um nome

A variável livre

Em saudar, o nome nome não é parâmetro nem variável local dela. Ele é uma variável livre: seu vínculo pertence ao escopo envolvente.

Ao avaliar um nome, Python procura primeiro no escopo local, depois no envolvente, em seguida no global e, por último, nos nomes embutidos. Neste exemplo, nome é encontrado no escopo envolvente.

Exemplo

O local da chamada não muda a origem

mensagem = "global"

para_bia = criar_saudacao("Bia")
print(para_bia())  # Olá, Bia!

Mesmo chamada fora de criar_saudacao, a função procura nome no ambiente em que foi definida. O local de chamada não transforma nome em uma variável global.

Identifique a variável livre

No código de criar_saudacao, qual é a variável livre da função saudar?

O que permanece — e o que não permanece

O retorno encerra a chamada externa

Depois de criar_saudacao("Ana") retornar, essa chamada externa terminou: ela não fica executando em segundo plano. Ainda assim, a closure conserva acesso aos vínculos de que precisa, como nome → "Ana".

A captura não cria automaticamente uma cópia ou uma “fotografia” do valor. A closure conserva acesso ao vínculo do ambiente envolvente. Essa diferença será importante ao trabalhar com estados mutáveis.

Explique o resultado

Por que para_ana() ainda consegue usar nome depois que criar_saudacao terminou?

Passo 3 de 7

Atualizar estado com nonlocal

Crie um contador que preserva seu estado entre chamadas e use nonlocal para atualizar o vínculo capturado.

Um contador que lembra do último valor

Estado criado uma vez

A função externa inicializa total uma única vez. A função proximo é retornada e, quando chamada, deve atualizar esse mesmo vínculo para que o contador não recomece do zero.

Onde o estado fica

O total pertence ao escopo de criar_contador; cada chamada posterior de proximo consulta e atualiza esse estado envolvente.

Diagrama de uma função externa contendo o vínculo total e uma função interna proximo; setas mostram chamadas sucessivas alterando total de zero para um, dois e três.

O corpo da função externa já terminou, mas o vínculo total continua disponível para a closure.

A intenção do contador

Esta versão expressa a atualização desejada, mas ainda falha ao ser chamada.

python
def criar_contador():
    total = 0

    def proximo():
        total += 1
        return total

    return proximo

contar = criar_contador()
print(contar())

Por que o incremento falha?

Atenção

Atribuir muda a regra de escopo

Dentro de proximo, a atribuição em total += 1 faz Python tratar total como um nome local desse corpo inteiro. Portanto, ao avaliar o lado direito de total += 1, ele tenta ler esse local antes de receber um valor e lança UnboundLocalError.

Ler não é reatribuir

Uma função interna pode apenas ler um vínculo envolvente sem declaração especial. Já uma atribuição ao nome — inclusive += — cria um vínculo local por padrão. Para reatribuir o vínculo da função externa, declare nonlocal total antes de usar ou atribuir esse nome.

Leitura permitida sem nonlocal

Aqui não há atribuição a mensagem; a função interna só consulta o vínculo envolvente.

python
def criar_saudacao():
    mensagem = "Olá"

    def mostrar():
        return mensagem

    return mostrar

print(criar_saudacao()())  # Olá

Reatribua o vínculo envolvente

Contador funcional

nonlocal total seleciona um vínculo de função envolvente já existente — neste caso, o mais próximo que se chama total. Ele não cria uma variável e não se refere a uma variável global.

python
def criar_contador():
    total = 0

    def proximo():
        nonlocal total
        total += 1
        return total

    return proximo

contar = criar_contador()
print(contar())  # 1
print(contar())  # 2
print(contar())  # 3

Complete a declaração

Para que proximo atualize o total criado por criar_contador, escreva a palavra que completa: _____ total

Confira a persistência no seu Python

Execute e observe

No seu computador, execute o código do contador funcional da tela anterior. Quais valores foram impressos? Em uma frase, explique o que isso revela sobre o vínculo total entre as chamadas.

Escreva pelo menos 30 caracteres (0/30).

Passo 4 de 7

Criar estados independentes

Veja como cada chamada de uma fábrica cria uma closure com seu próprio estado e como isso difere de apenas dar outro nome à mesma função.

Uma chamada, um ambiente próprio

A fábrica cria contadores

Uma função que cria e retorna outra função pode funcionar como uma fábrica. Cada chamada da fábrica executa novamente a inicialização de total e devolve uma nova closure.

Assim, criar_contador() chamado duas vezes produz dois contadores: cada um conserva seu próprio vínculo para total.

Fábrica de contadores

python
def criar_contador():
    total = 0

    def contar():
        nonlocal total
        total += 1
        return total

    return contar

primeiro = criar_contador()
segundo = criar_contador()

Dois ambientes capturados

Diagrama mostrando duas chamadas separadas da fábrica. A primeira produz a closure primeiro ligada ao seu próprio total iniciado em zero; a segunda produz a closure segundo ligada a outro total iniciado em zero.

Cada chamada de criar_contador() forma um ambiente separado, mesmo usando o mesmo código da função.

Chamadas intercaladas não misturam estados

Acompanhe cada contador

As chamadas podem ser intercaladas. Ainda assim, cada closure atualiza somente o total criado na chamada da fábrica que a produziu.

Rastreamento das chamadas

python
print(primeiro())  # 1  → estado de primeiro: 1
print(primeiro())  # 2  → estado de primeiro: 2
print(segundo())   # 1  → estado de segundo: 1
print(primeiro())  # 3  → estado de primeiro: 3
print(segundo())   # 2  → estado de segundo: 2

Estados em paralelo

Linha do tempo com chamadas alternadas: o contador azul avança de zero para um, dois e três; o contador laranja avança independentemente de zero para um e dois.

Intercalar chamadas altera a ordem de uso, mas não une os estados.

Novo nome não significa novo contador

Referência à mesma closure

Compare com esta atribuição:

apelido = primeiro

Aqui, apelido e primeiro referenciam a mesma função retornada. Não há nova chamada a criar_contador(), portanto não há novo total. Chamar um nome continua o estado observado pelo outro.

Preveja a saída

Considere:

a = criar_contador()
b = criar_contador()
c = a

print(a())
print(b())
print(c())
print(a())

Qual é a saída?

Regra para decidir

Resumo

Estados independentes ou compartilhados?

Use a origem da referência para identificar o estado.

  • Cada chamada de criar_contador() executa total = 0 em um novo ambiente e retorna uma nova closure.
  • Closures criadas por chamadas distintas da fábrica mantêm estados independentes.
  • Atribuir uma closure já existente a outro nome não chama a fábrica novamente.
  • Dois nomes para a mesma closure observam e atualizam o mesmo contador.

Dica

Pergunta-chave

Para saber se existe um novo estado, pergunte: houve uma nova chamada à fábrica? Se não houve, provavelmente há apenas uma nova referência à closure existente.

Passo 5 de 7

Distinguir mutação de reatribuição

Saiba identificar quando uma closure modifica a lista capturada e quando troca o vínculo para outra lista.

O nome e o objeto são coisas diferentes

Uma lista capturada pode mudar

Uma lista criada na função externa pode ser usada como estado de uma closure. Há duas operações diferentes:

  • Mutar: alterar o objeto ao qual itens já se refere, como em itens.append(...) ou itens.clear().
  • Reatribuir: fazer o nome itens apontar para outro objeto, como em itens = [].

A mutação não atribui ao nome itens; por isso, não precisa de nonlocal. A reatribuição muda o vínculo do nome e precisa de nonlocal.

Mutação versus reatribuição

Compare o que acontece com o vínculo capturado em cada caso.

Diagrama comparando uma variável itens apontando para uma lista. À esquerda, append adiciona um elemento à mesma lista. À direita, uma atribuição faz itens apontar para uma nova lista.

append modifica a lista existente; = troca o objeto para o qual o nome aponta.

Operações que modificam a lista

Mutar sem nonlocal

A função interna lê o vínculo itens e chama métodos no objeto ao qual ele aponta.

python
def criar_lista():
    itens = []

    def adicionar(item):
        itens.append(item)  # modifica a lista existente

    def limpar():
        itens.clear()       # modifica a mesma lista

    def consultar():
        return itens

    return adicionar, limpar, consultar

adicionar, limpar, consultar = criar_lista()
adicionar("caderno")
adicionar("caneta")
print(consultar())  # ['caderno', 'caneta']

limpar()
print(consultar())  # []

A mudança é visível por referências compartilhadas

consultar() retorna a própria lista capturada. Portanto, se outra referência receber esse retorno, ela observará mutações posteriores feitas por adicionar ou limpar: a closure não cria uma cópia automática da lista.

Trocar a lista exige nonlocal

Para substituir a lista inteira, declare nonlocal antes de usar ou atribuir itens nesse corpo de função.

python
def criar_lista():
    itens = ["rascunho"]

    def reiniciar():
        nonlocal itens
        itens = []           # agora itens aponta para outra lista

    def consultar():
        return itens

    return reiniciar, consultar

reiniciar, consultar = criar_lista()
reiniciar()
print(consultar())  # []

Atenção

Atenção ao +=

itens += ["novo"] também é uma atribuição ao nome itens. Mesmo que listas possam ser alteradas no lugar nessa operação, dentro da função interna você deve usar nonlocal itens. Sem isso, Python trata itens como nome local e a operação falha ao tentar ler esse nome antes da atribuição.

Classifique a operação

Mutação ou reatribuição?

Associe cada operação dentro de uma função interna à regra de escopo correspondente.

Toque em um item e depois no par correspondente.

Passo 6 de 7

Compartilhar estado entre funções internas

Crie um par de funções que consulta e atualiza o mesmo estado local de uma única fábrica.

Um estado, duas operações

Duas funções podem capturar o mesmo vínculo

Uma chamada da fábrica pode criar mais de uma função interna. Se ambas usam a variável saldo definida na função externa, elas acessam o mesmo vínculo capturado.

No exemplo, consultar apenas lê saldo; adicionar troca seu valor por um novo. Por isso, somente adicionar precisa declarar nonlocal saldo.

Estado compartilhado no mesmo par

Diagrama mostrando uma chamada de criar_conta gerando as funções consultar e adicionar, ambas conectadas ao mesmo recipiente de estado saldo; consultar lê o valor e adicionar o atualiza.

As duas funções retornadas pela mesma chamada externa apontam para o mesmo vínculo saldo.

Construindo e usando o par

Fábrica de operações de saldo

Cada chamada de criar_conta retorna uma tupla com duas funções.

python
def criar_conta(saldo_inicial):
    saldo = saldo_inicial

    def consultar():
        return saldo

    def adicionar(valor):
        nonlocal saldo
        saldo += valor

    return consultar, adicionar

consultar_a, adicionar_a = criar_conta(10)

adicionar_a(5)
print(consultar_a())  # 15

Exemplo

A ordem das chamadas importa

Depois de consultar_a, adicionar_a = criar_conta(10), as duas variáveis guardam funções do mesmo par.

  1. adicionar_a(5) reatribui o vínculo capturado: saldo passa de 10 para 15.
  2. consultar_a() lê esse mesmo vínculo e retorna 15.

A função externa já terminou, mas o vínculo capturado continua disponível para as funções retornadas.

Outro par, outro estado

Chamadas distintas criam ambientes distintos

Crie dois pares e intercale as operações.

python
consultar_a, adicionar_a = criar_conta(10)
consultar_b, adicionar_b = criar_conta(100)

adicionar_a(5)
adicionar_b(20)
adicionar_a(2)

print(consultar_a())  # 17
print(consultar_b())  # 120

Compartilhado dentro; separado entre pares

No par A, consultar_a e adicionar_a compartilham o saldo iniciado em 10. No par B, as funções compartilham outro saldo, iniciado em 100.

Assim, duas funções retornadas pela mesma chamada compartilham estado. Já uma nova chamada da fábrica cria novos vínculos locais e, portanto, um estado independente.

Dica

Leitura não é reatribuição

consultar não usa nonlocal porque apenas consulta saldo. A declaração é necessária em adicionar porque saldo += valor reatribui o nome saldo.

Preveja o estado compartilhado

Explique o resultado

Considere:

<code>def criar_operacoes():
total = 7

def ler():
return total

def incrementar():
nonlocal total
total += 3

return ler, incrementar

ler, incrementar = criar_operacoes()
incrementar()
resultado = ler()</code>

Qual é o valor de resultado? Explique por que as duas funções observam o mesmo estado e por que apenas incrementar declara nonlocal.

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 7

Aplicar e revisar: um registrador com estado

Integre closures, nonlocal e mutação em uma fábrica de registradores com estado privado e independente.

Contrato e estrutura da fábrica

Um registrador por chamada

Crie uma função criar_registrador() que inicializa dois vínculos locais: total, começando em 0, e historico, começando como uma lista vazia. Ela retorna duas funções: registrar(valor) e consultar().

  • registrar(valor) soma o valor ao total e adiciona esse valor ao histórico.
  • consultar() devolve o total atual e uma cópia do histórico.

As duas funções retornadas pela mesma chamada compartilham os mesmos vínculos capturados. Uma nova chamada da fábrica cria outro conjunto de vínculos, sem usar variáveis globais.

Um par compartilha; outro é independente

Observe quais operações acessam cada estado.

Diagrama de duas chamadas independentes à fábrica. No primeiro par, registrar e consultar apontam para o mesmo total e histórico; no segundo par, apontam para outro total e outro histórico.

No mesmo par, as operações compartilham estado. Entre pares criados por chamadas distintas, o estado é separado.

Dica

Duas operações, duas regras de escopo

total += valor reatribui o nome total; por isso, registrar precisa declarar nonlocal total. Já historico.append(valor) modifica a lista existente, sem reatribuir o nome historico; portanto, não precisa de nonlocal.

Implemente e execute localmente

Prática no seu computador

Em um arquivo Python novo, escreva o código a seguir por conta própria e execute-o com Python 3. Os assert não exibem nada quando passam; se algum falhar, revise o estado esperado antes de comparar sua solução com a referência da próxima tela.

Roteiro de verificações

Acrescente estes testes depois da sua implementação.

python
registrar_a, consultar_a = criar_registrador()

assert consultar_a() == (0, [])

registrar_a(10)
registrar_a(-3)
assert consultar_a() == (7, [10, -3])

# A lista recebida é uma cópia: alterá-la não muda o histórico interno.
total_a, copia_a = consultar_a()
copia_a.append(999)
assert consultar_a() == (7, [10, -3])

# Outra chamada à fábrica cria estado independente.
registrar_b, consultar_b = criar_registrador()
registrar_b(50)
assert consultar_b() == (50, [50])
assert consultar_a() == (7, [10, -3])

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

Confira uma solução de referência

Implementação completa

Compare a estrutura e o comportamento, não apenas a aparência do código.

python
def criar_registrador():
    total = 0
    historico = []

    def registrar(valor):
        nonlocal total
        total += valor
        historico.append(valor)

    def consultar():
        return total, historico.copy()

    return registrar, consultar


registrar_a, consultar_a = criar_registrador()

assert consultar_a() == (0, [])
registrar_a(10)
registrar_a(-3)
assert consultar_a() == (7, [10, -3])

total_a, copia_a = consultar_a()
copia_a.append(999)
assert consultar_a() == (7, [10, -3])

registrar_b, consultar_b = criar_registrador()
registrar_b(50)
assert consultar_b() == (50, [50])
assert consultar_a() == (7, [10, -3])

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

Relate sua verificação

Após executar os assert, relate o que observou. Explique por que total usa nonlocal, por que historico.append(valor) não usa e como os testes demonstram a independência entre os dois registradores.

Escreva pelo menos 100 caracteres (0/100).

Síntese e conclusão

Resumo

O que você consolidou

  • Uma função externa pode retornar funções internas que conservam acesso aos vínculos criados naquela chamada.
  • Funções internas retornadas juntas podem consultar e atualizar o mesmo estado privado.
  • Use nonlocal quando uma função interna precisa reatribuir um vínculo de um escopo envolvente, como em total += valor.
  • Modificar um objeto capturado, como com historico.append(valor), não reatribui o nome e não exige nonlocal.
  • Cada nova chamada da fábrica cria um ambiente independente; retornar historico.copy() evita expor a lista interna.

Tutorial concluído

Parabéns! Você concluiu: Criar closures com estado e nonlocal

Você criou e verificou closures com estado: funções retornadas, vínculos persistentes, nonlocal, mutação controlada e ambientes independentes. No próximo tutorial, você verá um caso importante: a vinculação tardia em closures criadas em laços.

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