Creating functions (or lambdas) in a loop (or comprehension)
Создание функций (или лямбд) в цикле (или понимание)
Я пытаюсь создавать функции внутри цикла:
functions = []
for i inrange(3): deff(): 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 соответственно?
Переведено автоматически
Ответ 1
Вы столкнулись с проблемой с поздней привязкой - каждая функция запускается i как можно позже (таким образом, при вызове после окончания цикла, i будет установлено значение 2).
Легко исправлено принудительным ранним привязыванием: измените def f(): на def f(i=i): вот так:
deff(i=i): return i
Значения по умолчанию (правый i in i=i - это значение по умолчанию для имени аргумента i, которое является левым i in i=i) ищутся во def время, а не в call момент времени, поэтому, по сути, это способ специального поиска ранней привязки.
Если вы беспокоитесь о f получении дополнительного аргумента (и, следовательно, возможном ошибочном вызове), есть более сложный способ, который включает использование замыкания в качестве "фабрики функций":
defmake_f(i): deff(): return i return f
и в вашем цикле используйте f = make_f(i) вместо def инструкции.
Ответ 2
Объяснение
Проблема здесь в том, что значение i не сохраняется при создании функции f. Скорее, f ищет значениеi, когда оно вызывается.
Если подумать, такое поведение имеет смысл. Фактически, это единственный разумный способ работы функций. Представьте, что у вас есть функция, которая обращается к глобальной переменной, например, так:
global_var = 'foo'
defmy_function(): print(global_var)
global_var = 'bar' my_function()
Когда вы читаете этот код, вы, конечно, ожидаете, что он напечатает "bar", а не "foo", потому что значение global_var изменилось после объявления функции. То же самое происходит и в вашем собственном коде: к моменту вызова f значение i изменилось и было установлено равным 2.
Решение
На самом деле существует много способов решить эту проблему. Вот несколько вариантов:
Принудительно выполнить раннее связывание i, используя его в качестве аргумента по умолчанию
В отличие от переменных закрытия (например, i), аргументы по умолчанию вычисляются сразу после определения функции:
for i inrange(3): deff(i=i): # <- right here is the important bit return i
functions.append(f)
Чтобы дать небольшое представление о том, как / почему это работает: аргументы функции по умолчанию хранятся как атрибут функции; таким образом, текущее значение i снимается моментально и сохраняется.
>>> i = 0 >>> deff(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 это переменная, которая может изменяться. Мы можем обойти эту проблему, создав другую переменную, которая гарантированно никогда не изменится - и самый простой способ сделать это - закрытие:
deff_factory(i): deff(): return i # i is now a *local* variable of f_factory and can't ever change return f
for i inrange(3): f = f_factory(i) functions.append(f)
Используйте functools.partial для привязки текущего значения i к f
functools.partial позволяет присоединять аргументы к существующей функции. В некотором смысле, это тоже своего рода фабрика функций.
import functools
deff(i): return i
for i inrange(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 >>> deff(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())
Ответ 3
Для тех, кто обращается к этому вопросу с помощью lambda:
Решение состоит в простой замене lambda: i на lambda i=i: i.
functions = [] for i inrange(3): functions.append(lambda i=i: i) print([f() for f in functions]) # [0, 1, 2]
Чтобы дополнить отличный ответ @Aran-Fey, во втором решении вы также можете захотеть изменить переменную внутри вашей функции, что может быть выполнено с помощью ключевого слова nonlocal:
deff_factory(i): deff(offset): nonlocal i i += offset return i # i is now a *local* variable of f_factory and can't ever change return f