Пояснення
Проблема тут полягає в тому, що значення параметра 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())