在循环(或推导式)中创建函数(或lambda)
我正在尝试在一个循环里创建函数:
functions = []
for i in range(3):
def f():
return i
functions.append(f)
另外,也可以用lambda表达式:
functions = []
for i in range(3):
functions.append(lambda: i)
问题是,所有的函数最后都变成了一样的。它们都返回2,而不是返回0、1和2:
print([f() for f in functions])
- 期望的输出:
[0, 1, 2]
- 实际的输出:
[2, 2, 2]
为什么会这样?我该怎么做才能得到三个不同的函数,分别输出0、1和2呢?
9 个回答
对于那些使用 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]
解释
这里的问题是,当函数 f
被创建时,i
的值并没有被保存。相反,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())
你遇到的问题是关于延迟绑定的——每个函数在调用时会尽可能晚地查找i
(所以当在循环结束后调用时,i
的值会变成2
)。
这个问题很容易解决,只需要强制使用早期绑定:把def f():
改成def f(i=i):
,像这样:
def f(i=i):
return i
默认值(i=i
右边的i
是参数名i
的默认值,而左边的i
是i=i
中的左侧i
)是在定义函数时查找的,而不是在调用时查找的,所以本质上这是为了确保使用早期绑定。
如果你担心f
会多接一个参数(这样可能会导致错误调用),还有一种更复杂的方法,就是使用闭包作为“函数工厂”:
def make_f(i):
def f():
return i
return f
然后在你的循环中用f = make_f(i)
来代替def
语句。