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