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

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

66 голосов
/ 07 августа 2010

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

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 соответственно?

Ответы [ 2 ]

120 голосов
/ 07 августа 2010

Вы столкнулись с проблемой с поздним связыванием - каждая функция ищет 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.

12 голосов
/ 10 марта 2019

Объяснение

Проблема здесь в том, что значение 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())
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...