Вопрос-Ответ

Creating functions (or lambdas) in a loop (or comprehension)

Создание функций (или лямбд) в цикле (или понимание)

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

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 in i=i - это значение по умолчанию для имени аргумента i, которое является левым i in 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'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())

Ответ 3

Для тех, кто обращается к этому вопросу с помощью lambda:

Решение состоит в простой замене lambda: i на lambda i=i: i.

functions = []
for i in range(3):
functions.append(lambda i=i: i)
print([f() for f in functions])
# [0, 1, 2]

Пример использования: Как заставить лямбда-функцию вычислять переменную сейчас (а не откладывать)

Ответ 4

Чтобы дополнить отличный ответ @Aran-Fey, во втором решении вы также можете захотеть изменить переменную внутри вашей функции, что может быть выполнено с помощью ключевого слова nonlocal:

def f_factory(i):
def f(offset):
nonlocal i
i += offset
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)
print(f(10))
2023-03-27 11:41 python