Створення функцій у циклі


102

Я намагаюся створити функції всередині циклу:

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 відповідно?


4
як нагадування про себе: docs.python-guide.org/en/latest/writing/gotchas/…
Lu

Відповіді:


167

У вас виникає проблема із запізнілим прив’язкою - кожна функція виглядає 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оператора.


7
як ти знаєш, як виправити ці речі?
alwbtc

3
@alwbtc це здебільшого просто досвід, більшість людей в якийсь момент стикалися з цими речами самостійно.
ruohola

Чи можете ви пояснити, чому це працює, будь ласка? (Ви рятуєте мене від зворотного дзвінка, згенерованого в циклі, аргументи завжди були тривалістю циклу, тож дякую!)
Вінсент Бенет,

20

Пояснення

Проблема тут полягає в тому, що значення параметра iне зберігається під час створення функції f. Швидше, fшукає значення, iколи воно викликається .

Якщо ви задумаєтесь, така поведінка має ідеальний сенс. Насправді це єдиний розумний спосіб роботи функцій. Уявіть, що у вас є функція, яка отримує доступ до глобальної змінної, наприклад:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Читаючи цей код, ви, звичайно, очікуєте, що він надрукує "бар", а не "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)

Caveat: Ці рішення працюють лише в тому випадку, якщо ви присвоїли нове значення змінній. Якщо ви зміните об'єкт, що зберігається у змінній, ви знову відчуєте ту ж проблему:

>>> 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())
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.