Tag Archives: closure

Pegadinha com closures (ilustrado em Python)

(Embora ilustrado em Python, creio que conhecer Python não seja totalmente necessário para entender o post :) )

Você conhece o conceito de closures (“fecho”, mas acho que ninguém usa o termo em português…)? Basicamente, é quando você pode definir uma função em tempo de execução e esta função faz referência a variáveis de um escopo externo a ela. Por exemplo:

def make_number_printer(n):
    def number_printer():
        print n
    return number_printer
 
printer = make_number_printer(5)
printer()

A função make_number_printer recebe um número e retorna uma função que, quando chamada, imprime esse mesmo número. Não é a função mais útil do universo, mas ilustra bem o conceito de closure. No caso, a função number_printer é uma closure, pois referencia a variável n que está num escopo externo (o da função make_number_printer).

Porém minha intenção aqui não é tanto explicar o que são closures, mas sim ilustrar uma característica que pode acabar confundindo (já perdi um bom tempo caçando um bug causado por isso). O que esse código imprime?

printer_lst = []
for i in xrange(10):
    def number_printer():
        print i
    printer_lst.append(number_printer)
 
for printer in printer_lst:
    printer()

Ele executa um loop de 0 a 9, em cada iteração criando uma função number_printer e colocando-a numa lista, e em seguida percorre essa lista chamando todas as funções. Era de se esperar que ele imprimisse os números de 0 a 9, certo?

Errado! Ele imprime o número 9 dez vezes.

O problema é que na maioria das vezes, intuitivamente pensa-se que as closures funcionam “fixando” o valor das variáveis externas que elas referenciam para usá-las num momento posterior. Mas na verdades elas guardam uma referência àquela variável, e se ela mudar de valor, a closure usará o valor alterado. Como a nossa variável i vale 9 no final da criação das closures, esse é o valor que todas vão imprimir. Isso vale mesmo se a variável i deixar de existir após a criação das closures (por exemplo, se o primeiro for estivesse dentro de uma função).

Como resolver isso? Você pode mover a criação da closure para uma outra função, desta forma:

def make_number_printer(n):
    def number_printer():
        print n
    return number_printer
 
printer_lst = []
for i in xrange(10):
    printer_lst.append(make_number_printer(i))
 
for printer in printer_lst:
    printer()

Assim a closure irá referenciar a variável n, que possui instâncias diferentes para cada closure diferente criada. Outra alternativa é utilizar um “recurso” polêmico do Python, que é o fato de que os valores de parâmetros com valores padrão são avaliados quando a função é definida, e não quando ela é chamada:

printer_lst = []
for i in xrange(10):
    def number_printer(x=i):
        print x
    printer_lst.append(number_printer)
 
for printer in printer_lst:
    printer()

Ao executar def number_printer(x=i):, o valor de i é avaliado e salvo permanentemente na definição da função; assim, cada vez que a função é definida (a closure é criada) o valor atual de i é “congelado”.

Se alguém se estiver perguntando, “mas eu nunca vou acabar me deparando com isso”, este é um exemplo um pouco mais próximo da realidade (na verdade me bati com ele quando programava um jogo em Flash, que usa ActionScript). Fundamentalmente é o mesmo código acima e apresenta o mesmo problema:

class Button:
    #Esta é uma classe para simular um botão, suponha
    #que é parte de uma biblioteca de GUI e por alguma
    #razão você não pode herdar dela
 
    def __init__(self):
        self.listener = None
 
    def set_click_listener(self, fn):
        self.listener = fn
 
    def on_click(self):
        self.listener()
 
#Cria 10 botões...
buttons = [Button() for i in xrange(10)]
#E suponha que eles são adicionados na interface
 
#Seta os listeners para o evento de clique.
#O número de botões pode mudar no futuro
#e todos têm o mesmo código, a única diferença
#sendo o índice do botão. Então é melhor fazer isso
#dentro de um loop.
for i in xrange(10):
    def on_click():
        #Imagine que o código aqui seja mais útil
        print i
    buttons[i].set_click_listener(on_click)
 
#Simula um clique em cada botão
for j in xrange(10):
    buttons[j].on_click()

Closure gotcha (with Python)

Do you know what a closure is? Basically, it’s a function created at runtime that references variables defined in a outer scope. For example:

def make_number_printer(n):
    def number_printer():
        print n
    return number_printer
 
printer = make_number_printer(5)
printer()

The function make_number_printer receives a number and returns a function which, when called, prints that same number. It’s not the most useful function in the world, but it does show how closures work. In this case, the number_printer function is a closure, because it references the variable n which is in an outer scope (of the make_number_printer function).

My intention here, though, it’s not to explain what closures are but to show a property of them which may cause some confusion (and made me spent quite some time hunting for a bug caused by it). What does this code prints when run?

printer_lst = []
for i in xrange(10):
    def number_printer():
        print i
    printer_lst.append(number_printer)
 
for printer in printer_lst:
    printer()

It loops between 0 and 10 creating a number_printer function and appending it to a list. Then it loops through the functions of the list, calling them. You would expect it to print the numbers 0 to 9, right?

Wrong! It prints the number 9 ten times.

The problem is that most people (including me, before learning this) think that closures work by evaluating the variables in the outer scope and storing their values to use when necessary. But actually they keep an reference to those variables, and if their contents change, the closure will use the new value. Since our variable i stores the value 9 after the closures are created, that is the value they will print. This happens even if the variable i goes out of scope after the closures are created (e.g., if the first for were inside a function).

So how to solve this? You can move the creation of the closure to another function, like this:

def make_number_printer(n):
    def number_printer():
        print n
    return number_printer
 
printer_lst = []
for i in xrange(10):
    printer_lst.append(make_number_printer(i))
 
for printer in printer_lst:
    printer()

Here the closure will reference the variable n, which has different instances for each different closure created. Another alternative is to use a somewhat contrived Python “feature” which is the fact that default parameter values are evaluated when the function is defined and not when they are called:

printer_lst = []
for i in xrange(10):
    def number_printer(x=i):
        print x
    printer_lst.append(number_printer)
 
for printer in printer_lst:
    printer()

When def number_printer(x=i): is run, the variable i is evaluated and its value is saved in the function definition; so, each time the function is defined (i.e., the closure is created), the current value of i is “frozen”.

If somebody is thinking, “but I’ll never run into this situation”, here is a little more real example (which actually happened to me when I was coding a Flash game, which uses ActionScript). Basically it’s the same code above and has the same issue:

class Button:
    #This is a dummy Button class; suppose
    #it's part of a GUI library and for some reason
    #you can't subclass it
 
    def __init__(self):
        self.listener = None
 
    def set_click_listener(self, fn):
        self.listener = fn
 
    def on_click(self):
        self.listener()
 
#Create 10 buttons...
buttons = [Button() for i in xrange(10)]
#And suppose the buttons are added to the GUI after
 
#Set the listeners to the click event.
#The number of buttons may change in the future
#and all of them have the same code, the
#only difference is the button index. So it's better
#to do this within a loop. 
for i in xrange(10):
    def on_click():
        #Suppose there is something more useful here
        print i
    buttons[i].set_click_listener(on_click)
 
#Simulate a click in each button
for j in xrange(10):
    buttons[j].on_click()