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