Closures: um escopo local persistente

Closures: definição

Igor G. Peternella
Editora Globo
6 min readAug 15, 2018

--

Photo by Chris Ried on Unsplash

Olá! Neste post eu vou explicar o que é uma closure e o porquê desse nome! Vamos usar algumas ideias discutidas no post anterior para, finalmente, aprendermos o que são clousures e como usá-las!

Ahn? Pera ai, não viu o post anterior? Como assim? Dê uma olhadinha nele antes:

https://medium.com/editora-globo/closures-um-escopo-local-persistente-51003148cac6

Vou confiar que você leu, hein!? Mas, de qualquer forma, aqui vai um breve resumo sobre alguns pontos importantes:

  • Python, como muitas linguagens, é implementada para utilizar escopo léxico, fato que permite que os escopos definidos por funções, por exemplo, sejam criados baseados no código-fonte apenas (ou seja, no contexto léxico);
  • Python implementa funções como cidadãos de primeira classe o que nos permite criar funções dentro de outras funções, passá-las como argumentos ou até mesmo criar funções que retornam outras funções.

Tá, e dai? Vamos usar estes dois conceitos para criarmos uma closure!

1. Definição:

Antes de tudo, vamos nos atentar ao seguinte detalhe:

wrapper_context é uma função que define outra função em seu interior além de retorná-la. Logo, wrapper_context é uma função de ordem superior que utiliza o fato de funções serem implementadas como cidadãos de primeira classe pela linguagem Python.

Agora, vamos à parte do escopo léxico:

sample_closure foi definida em um escopo ou contexto onde x = 2. Lembremos que este escopo é definido pelo código-fonte, já que estamos falando de escopo estático.

Agora, podemos, finalmente apreciar o que é uma closure e a importância desse escopo que tanto falo:

NÃO importa o local ou contexto onde invocamos a função sample_closure, esta sempre printará o valor 2 pois tal função foi definida num contexto estático (definido no código-fonte) no qual x tinha o valor 2.

Com isso, podemos entender algumas coisas:

  • Na linha 33, invocamos wrapper_context que retorna a função sample_closure a qual pode ser invocada através da variável closure.
  • Na linha 37, invocamos a função sample_closure. Note que tal função foi invocada no contexto global onde x = 20, mas isso não importa: a função sample_closure sempre se “lembrará” do seu ambiente onde foi definida, isto é, onde x = 2. Logo, a linha 37 printará o valor 2.
  • Analogamente, passamos a função sample_closure como argumento para uma outra função chamada another_context. Note que another_context define um escopo onde x = 10. Novamente, isso não importa pois sample_closure se lembra do valor x = 2. Portanto, a linha 41 printará o valor 2.

Assim, podemos invocar a função sample_closure em qualquer contexto sem medo, pois ela lembrará da variável x = 2.

Vejamos, agora, uma definição mais “formal” de uma closure pela Wikipedia:

In programming languages, a closure (also lexical closure or function closure) is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function[a] together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. A closure — unlike a plain function — allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.

Um pouco complexo, né? Vamos resumir isso da seguinte forma:

Closure é uma técnica utilizada em linguagens que implementam escopo léxico. Basicamente, closure é a técnica de armazenar a definição de uma função junto com o ambiente onde tal função foi definida. Como sabemos, tal contexto ou escopo é definido estaticamente, isto é, através do código fonte.

Isso explica o porquê da função sample_closure sempre se lembrar de x = 2 já que tal função é armazenda junto ao ambiente (onde x = 2) pelo interpretador Python!

Mas, qual o motivo do nome cláusura (closure)? Isso me lembra algo enclausurado… fechado, não? Para entender isso, precisamos voltar um pouco atrás na nossa discussão para ver como o contexto estático é essencial para compreender esse nome.

Quando invocamos a função wrapper_context, obtemos a função sample_closure que deve printar o valor da variável x, certo? Vamos relembrar esse pedaço do código:

Quando o interpretador Python for avaliar (determinar o resultado) de uma invocação da função sample_closure, encontramos um problema uma vez que o valor de x não está definido dentro da função! Como o interpretador irá produzir o resultado do valor de x se este não é definido?

Dizemos, então, que x é uma variável aberta (open variable). Expressões que possuem variáveis abertas são denominadas expressões abertas e não podem ser avaliadas já que, na verdade, existem variáveis indefinidas.

Entretanto, sabemos da nossa discussão que não importa o ambiente onde invocamos sample_closure uma vez que esta função sempre printa o valor x = 2! Mas, pera ai! Então o interpretador Python conseguiu avaliar uma expressão que deveria ser aberta, isto é, que possui o valor x indefinido?

É quase isso! Só que na verdade essa expressão não era aberta… ela foi, na verdade, fechada, isto é, o valor de x foi definido… e advinha por quem?

Isso mesmo! Foi o contexto léxico, o qual é armazenado junto com a definição da função, que forneceu um valor para a variável aberta x de forma a torná-la uma variável fechada ou enclasurada pelo ambiente léxico!

E esta é a grande ideia por trás das closures! Como as funções são armazenadas pelo processador da linguagem junto ao contexto léxico onde foram definidas, temos que este ambiente externo na definiçáo função é quem enclausura ou fecha todas as variáveis abertas de forma que o local da invocação função se torna irrelevante, já que é o ambiente onde a função foi definida é quem irá ditar os valores das variáveis “indefinidas”!

2. Aplicações

Podemos, agora, entender o título desta série: closures são uma forma de criar um escopo local persistente! Assim, funções, independentemente do local de sua invocação, irão se “lembrar” de variáveis definidas no contexto ou do escopo local onde tais funções forma criadas! Acho que esse é um grande resumo da definição de closures!

Vamos aplicar? Vejamos este pequeno pedaço de código:

Na linha 4, notamos que a função append_to_list possui a variável xs em seu interior, mas que não é definida dentro desta função. Assim, para xs ter um significado, é necessário um contexto externo à função! Este contexto será o que irá "enclausurar" a função append_to_list em um ambiente onde xs tem um valor definido!

Na linha 15, quando invocamos get_list_closure, obtemos a função append_to_list em um contexto onde xs = [1, 2, 3], de forma que esta lista será “lembrada” pela função get_list_closure! Entretanto, quando invocamos list_appender com novos argumentos:

Observamos que a lista inicial começa a aumentar e surge a dúvida: a função get_list_closure não deveria sempre se lembrar do objeto [1, 2, 3]? Afinal esta foi definida em ambiente onde xs = [1, 2, 3], não?!

Isso é verdade! E é exatamente o que acontece! get_list_closure sempre referenciará o mesmo objeto lista em memória. Contudo, esta estrutura de dados em Python é mutável e, assim, pode ser alterada para [1,2,3,4], etc. Portanto, apesar de parecer como novas listas, estas são o mesmo objeto em memória, mas que foram mutadas para um novo valor. Tal fato seria diferente se tivéssemos utilizado uma estrutura de dados imutável como tuplas.

No próximo post, iremos utilizar essas ideias para entender os decorators da linguagem Python bem como aprender o que é açúcar sintático!

Photo by rawpixel on Unsplash

Em caso de dúvidas, erros ou sugestões, por favor entrem em contato! Discussões são sempre bem vindas! Até lá!

--

--