提问人:sharvey 提问时间:8/8/2010 最后编辑:jonrsharpesharvey 更新时间:6/7/2023 访问量:102797
在循环(或推导)中创建函数(或 lambda)
Creating functions (or lambdas) in a loop (or comprehension)
问:
我正在尝试在循环中创建函数:
functions = []
for i in range(3):
def f():
return i
# alternatively: f = lambda: i
functions.append(f)
问题是所有功能最终都是相同的。所有三个函数都返回 2,而不是返回 0、1 和 2:
print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output: [2, 2, 2]
为什么会这样,我应该怎么做才能获得分别输出 0、1 和 2 的 3 个不同函数?
答:
您遇到了延迟绑定的问题 -- 每个函数都尽可能晚地查找(因此,在循环结束后调用时,将设置为 )。i
i
2
通过强制提前绑定轻松修复:更改为如下所示:def f():
def f(i=i):
def f(i=i):
return i
默认值(右边是参数名称的默认值,左边是 in)是在时间而不是在时间上查找的,因此从本质上讲,它们是一种专门查找早期绑定的方法。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
评论
解释
这里的问题是,创建函数时不会保存 的值。相反,查找调用时的值。i
f
f
i
如果你仔细想想,这种行为是完全有道理的。事实上,这是函数工作的唯一合理方式。假设你有一个访问全局变量的函数,如下所示:
global_var = 'foo'
def my_function():
print(global_var)
global_var = 'bar'
my_function()
当你阅读这段代码时,你当然会期望它打印“bar”,而不是“foo”,因为在函数声明后,值 of 发生了变化。同样的事情也发生在你自己的代码中:当你调用时,的值已经改变并被设置为。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())
评论
f_factory
i
f_factory
f
为了补充 @Aran-Fey 的出色答案,在第二个解决方案中,您可能还希望修改函数中的变量,这可以通过关键字完成:nonlocal
def f_factory(i):
def f(offset):
nonlocal i
i += offset
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)
print(f(10))
你可以像这样尝试:
l=[]
for t in range(10):
def up(y):
print(y)
l.append(up)
l[5]('printing in 5th function')
评论
只需修改最后一行
functions.append(f())
编辑:这是因为是一个函数 - python 将函数视为一等公民,您可以在变量中传递它们以供以后调用。所以你的原始代码正在做的是将函数本身追加到列表中,而你要做的是将函数的结果追加到列表中,这就是上面的行通过调用函数来实现的。f
评论
您必须将每个值保存在内存中的单独空间中,例如:i
class StaticValue:
val = None
def __init__(self, value: int):
StaticValue.val = value
@staticmethod
def get_lambda():
return lambda x: x*StaticValue.val
class NotStaticValue:
def __init__(self, value: int):
self.val = value
def get_lambda(self):
return lambda x: x*self.val
if __name__ == '__main__':
def foo():
return [lambda x: x*i for i in range(4)]
def bar():
return [StaticValue(i).get_lambda() for i in range(4)]
def foo_repaired():
return [NotStaticValue(i).get_lambda() for i in range(4)]
print([x(2) for x in foo()])
print([x(2) for x in bar()])
print([x(2) for x in foo_repaired()])
Result:
[6, 6, 6, 6]
[6, 6, 6, 6]
[0, 2, 4, 6]
对于那些使用以下方法回答这个问题的人: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]
上一个:泡菜蟒封口
评论
lambda: i
lambda i=i: i
for i in range(3): functions.append(lambda i=i: i)