在循环(或推导式)中创建函数(或lambda)

215 投票
9 回答
107130 浏览
提问于 2025-04-16 02:28

我正在尝试在一个循环里创建函数:

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 个回答

3

对于那些使用 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 函数立即计算一个变量,而不是延迟计算

70

解释

这里的问题是,当函数 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.partiali 的当前值绑定到 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())
270

你遇到的问题是关于延迟绑定的——每个函数在调用时会尽可能晚地查找i(所以当在循环结束后调用时,i的值会变成2)。

这个问题很容易解决,只需要强制使用早期绑定:把def f():改成def f(i=i):,像这样:

def f(i=i):
    return i

默认值(i=i右边的i是参数名i的默认值,而左边的ii=i中的左侧i)是在定义函数时查找的,而不是在调用时查找的,所以本质上这是为了确保使用早期绑定。

如果你担心f会多接一个参数(这样可能会导致错误调用),还有一种更复杂的方法,就是使用闭包作为“函数工厂”:

def make_f(i):
    def f():
        return i
    return f

然后在你的循环中用f = make_f(i)来代替def语句。

撰写回答