Создание функций в цикле

106

Я пытаюсь создать функции внутри цикла:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

Проблема в том, что все функции в конечном итоге одинаковы. Вместо того, чтобы возвращать 0, 1 и 2, все три функции возвращают 2:

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

Почему это происходит и что мне делать, чтобы получить 3 разных функции, которые выводят 0, 1 и 2 соответственно?

Шарви
источник
4
в качестве напоминания себе: docs.python-guide.org/en/latest/writing/gotchas/…
Чунтао Лу

Ответы:

170

Вы сталкиваетесь с проблемой позднего связывания - каждая функция выполняет поиск iкак можно позже (таким образом, при вызове после окончания цикла iбудет установлено значение 2).

Легко исправить путем принудительного раннего связывания: измените def f():на def f(i=i):это:

def f(i=i):
    return i

Значения по умолчанию (правая iв i=iэто значение по умолчанию для имени аргумента i, который является левосторонним iв i=i) ищутся на defвремя, а не наcall время, так что по существу они путь к специально искали раннее связывание.

Если вы беспокоитесь о fполучении дополнительного аргумента (и, следовательно, о том, что он может быть вызван ошибочно), есть более сложный способ, который предполагает использование замыкания в качестве «фабрики функций»:

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

и в вашем цикле используйте f = make_f(i)вместо defоператора.

Алекс Мартелли
источник
7
как вы знаете, как исправить эти вещи?
alwbtc
3
@alwbtc - это в основном просто опыт, большинство людей в какой-то момент сталкивались с этим самостоятельно.
ruohola 05
Вы можете объяснить, почему это работает? (Вы спасаете меня от обратного вызова, сгенерированного в цикле, аргументы всегда были последними, так что спасибо!)
Винсент Бенэ,
23

Объяснение

Проблема здесь в том, что значение iне сохраняется при создании функции f. Скорее, fищет значение iпри его вызове .

Если задуматься, такое поведение имеет смысл. Фактически, это единственный разумный способ работы функций. Представьте, что у вас есть функция, которая обращается к глобальной переменной, например:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Когда вы читаете этот код, вы, конечно, ожидаете, что он напечатает «bar», а не «foo», потому что значение global_varизменилось после того, как функция была объявлена. То же самое происходит в вашем собственном коде: к тому времени, когда вы вызываете f, значение iизменилось и было установлено на 2.

Решение

На самом деле есть много способов решить эту проблему. Вот несколько вариантов:

  • Принудительное раннее связывание i, используя его как аргумент по умолчанию

    В отличие от закрывающих переменных (например, i), аргументы по умолчанию оцениваются сразу после определения функции:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)
    

    Чтобы немного понять, как и почему это работает: аргументы функции по умолчанию хранятся как атрибут функции; таким образом, текущее значение iснимается и сохраняется.

    >>> 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,)
    
  • Используйте фабрику функций для захвата текущего значения iв замыкании

    Корень вашей проблемы в том, что iпеременная может изменяться. Мы можем обойти эту проблему, создав еще одну переменную, которая гарантированно никогда не изменится, и самый простой способ сделать это - закрыть :

    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)
    
  • Используется functools.partialдля привязки текущего значения iкf

    functools.partialпозволяет присоединять аргументы к существующей функции. В каком-то смысле это тоже своего рода фабрика функций.

    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)
    

Предупреждение: эти решения работают, только если вы присвоите переменной новое значение. Если вы измените объект, хранящийся в переменной, вы снова столкнетесь с той же проблемой:

>>> 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]

Обратите внимание, как iвсе еще изменилось, хотя мы превратили его в аргумент по умолчанию! Если ваш код мутирует i , то вы должны связать копию из iвашей функции, например , так:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
Аран-Фей
источник