Estou tentando criar funções dentro de um loop:
functions = []
for i in range(3):
def f():
return i
# alternatively: f = lambda: i
functions.append(f)
O problema é que todas as funções acabam sendo iguais. Em vez de retornar 0, 1 e 2, todas as três funções retornam 2:
print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output: [2, 2, 2]
Por que isso está acontecendo e o que devo fazer para obter 3 funções diferentes com saída 0, 1 e 2, respectivamente?
Respostas:
Você está tendo um problema com vinculação tardia - cada função procura o
i
mais tarde possível (portanto, quando chamada após o final do loop,i
será definida como2
).Pode ser facilmente corrigido ao forçar a vinculação inicial: mude
def f():
paradef f(i=i):
assim:Os valores padrão (da mão direita
i
emi=i
um valor padrão para o nome do argumentoi
, que é a mão esquerdai
emi=i
) são olhou paradef
o tempo, não nocall
tempo, de modo essencialmente eles são uma maneira de especificamente à procura de ligação antecipada.Se você está preocupado em
f
obter um argumento extra (e, portanto, potencialmente ser chamado erroneamente), há uma maneira mais sofisticada que envolve o uso de um encerramento como uma "fábrica de função":e em seu loop use em
f = make_f(i)
vez dadef
instrução.fonte
A explicação
O problema aqui é que o valor de
i
não é salvo quando a funçãof
é criada. Em vez disso,f
procura o valor dei
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:
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ê chamaf
, o valor dei
mudou e foi definido como2
.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ãoAo contrário das variáveis de fechamento (como
i
), os argumentos padrão são avaliados imediatamente quando a função é definida: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.Use uma fábrica de funções para capturar o valor atual de
i
em um fechamentoA 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 :Use
functools.partial
para ligar o valor atual dei
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.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:
Observe como
i
ainda mudou, embora o tenhamos transformado em um argumento padrão! Se seu código sofrer mutaçãoi
, você deve vincular uma cópia dei
à sua função, da seguinte forma:def f(i=i.copy()):
f = f_factory(i.copy())
f_with_i = functools.partial(f, i.copy())
fonte