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()

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*