A explicação
O problema aqui é que o valor de i
não é salvo quando a função f
é criada. Em vez disso, f
procura o valor de i
quando é chamado .
Se você pensar bem, esse comportamento faz todo o sentido. Na verdade, é a única maneira razoável pela qual as funções podem funcionar. Imagine que você tem uma função que acessa uma variável global, como esta:
global_var = 'foo'
def my_function():
print(global_var)
global_var = 'bar'
my_function()
Ao ler este código, você - é claro - esperaria que ele exibisse "bar", não "foo", porque o valor de global_var
mudou depois que a função foi declarada. A mesma coisa está acontecendo em seu próprio código: no momento em que você chama f
, o valor de i
mudou e foi definido como 2
.
A solução
Na verdade, existem muitas maneiras de resolver esse problema. Aqui estão algumas opções:
Force a vinculação inicial de i
usando-o como um argumento padrão
Ao contrário das variáveis de fechamento (como i
), os argumentos padrão são avaliados imediatamente quando a função é definida:
for i in range(3):
def f(i=i): # <- right here is the important bit
return i
functions.append(f)
Para dar uma pequena ideia de como / por que isso funciona: Os argumentos padrão de uma função são armazenados como um atributo da função; assim, o valor atual de i
é capturado e salvo.
>>> i = 0
>>> def f(i=i):
... pass
>>> f.__defaults__ # this is where the current value of i is stored
(0,)
>>> # assigning a new value to i has no effect on the function's default arguments
>>> i = 5
>>> f.__defaults__
(0,)
Use uma fábrica de funções para capturar o valor atual de i
em um fechamento
A raiz do seu problema é que i
é uma variável que pode mudar. Podemos contornar esse problema criando outra variável que nunca muda - e a maneira mais fácil de fazer isso é fechando :
def f_factory(i):
def f():
return i # i is now a *local* variable of f_factory and can't ever change
return f
for i in range(3):
f = f_factory(i)
functions.append(f)
Use functools.partial
para ligar o valor atual de i
af
functools.partial
permite anexar argumentos a uma função existente. De certa forma, também é uma espécie de fábrica de funções.
import functools
def f(i):
return i
for i in range(3):
f_with_i = functools.partial(f, i) # important: use a different variable than "f"
functions.append(f_with_i)
Advertência: essas soluções só funcionam se você atribuir um novo valor à variável. Se você modificar o objeto armazenado na variável, terá o mesmo problema novamente:
>>> i = [] # instead of an int, i is now a *mutable* object
>>> def f(i=i):
... print('i =', i)
...
>>> i.append(5) # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]
Observe como i
ainda mudou, embora o tenhamos transformado em um argumento padrão! Se seu código sofrer mutação i
, você deve vincular uma cópia de i
à sua função, da seguinte forma:
def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())