Я пытаюсь создать функции внутри цикла:
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 соответственно?
Ответы:
Вы сталкиваетесь с проблемой позднего связывания - каждая функция выполняет поиск
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
оператора.источник
Объяснение
Проблема здесь в том, что значение
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())
источник