Criação de funções em um loop

102

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?

Sharvey
fonte
4
como um lembrete para mim mesmo: docs.python-guide.org/en/latest/writing/gotchas/…
Chuntao Lu

Respostas:

167

Você está tendo um problema com vinculação tardia - cada função procura o imais tarde possível (portanto, quando chamada após o final do loop, iserá definida como 2).

Pode ser facilmente corrigido ao forçar a vinculação inicial: mude def f():para def f(i=i):assim:

def f(i=i):
    return i

Os valores padrão (da mão direita iem i=ium valor padrão para o nome do argumento i, que é a mão esquerda iem i=i) são olhou para defo tempo, não no calltempo, de modo essencialmente eles são uma maneira de especificamente à procura de ligação antecipada.

Se você está preocupado em fobter 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":

def make_f(i):
    def f():
        return i
    return f

e em seu loop use em f = make_f(i)vez da definstrução.

Alex Martelli
fonte
7
como você sabe como consertar essas coisas?
alwbtc
3
@alwbtc é mais apenas experiência, a maioria das pessoas já enfrentou essas coisas por conta própria em algum momento.
ruohola
Você pode explicar por que está funcionando, por favor? (Você me economiza no retorno de chamada gerado no loop, os argumentos sempre foram os últimos do loop, então obrigado!)
Vincent Bénet
20

A explicação

O problema aqui é que o valor de inão é salvo quando a função fé criada. Em vez disso, fprocura o valor de iquando é 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_varmudou 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 imudou 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 iusando-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 iem 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.partialpara ligar o valor atual de iaf

    functools.partialpermite 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 iainda 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())
Aran-Fey
fonte