问题:循环创建函数
我正在尝试在循环内创建函数:
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]
为什么会发生这种情况,我应该怎么做才能获得分别输出0、1和2的3个不同函数?
回答 0
您在后期绑定方面遇到了麻烦 -每个函数都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
语句。
回答 1
说明
这里的问题是创建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())