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

Creating functions (or lambdas) in a loop (or comprehension)

提问人:sharvey 提问时间:8/8/2010 最后编辑:jonrsharpesharvey 更新时间:6/7/2023 访问量:102797

问:

我正在尝试在循环中创建函数:

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 个不同函数?

评论

11赞 Skiptomylu 9/11/2014
提醒自己:docs.python-guide.org/en/latest/writing/gotchas/......
1赞 Karl Knechtel 8/18/2022
请注意,如果随后循环访问生成器并调用每个函数,则使用生成器时可能不会出现问题。这是因为一切都是懒惰地评估的,因此与绑定一样“迟到”。循环的迭代变量增加,立即创建下一个函数或 lambda,然后立即调用所述函数或 lambda - 使用当前迭代值。这同样适用于生成器表达式。有关示例,请参阅 stackoverflow.com/questions/49633868
0赞 Basj 6/7/2023
解决方案:替换为 。您的代码现在是 .lambda: ilambda i=i: ifor i in range(3): functions.append(lambda i=i: i)

答:

266赞 Alex Martelli 8/8/2010 #1

您遇到了延迟绑定的问题 -- 每个函数都尽可能晚地查找(因此,在循环结束后调用时,将设置为 )。ii2

通过强制提前绑定轻松修复:更改为如下所示:def f():def f(i=i):

def f(i=i):
    return i

默认值(右边是参数名称的默认值,左边是 in)是在时间而不是在时间上查找的,因此从本质上讲,它们是一种专门查找早期绑定的方法。ii=iiii=idefcall

如果你担心得到一个额外的参数(因此可能会被错误地调用),有一种更复杂的方法,涉及使用闭包作为“函数工厂”:f

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

在你的循环中,使用而不是语句。f = make_f(i)def

评论

16赞 alwbtc 8/18/2018
你怎么知道如何解决这些问题?
10赞 ruohola 3/6/2019
@alwbtc这主要只是经验,但大多数人在某个时候都独自面对过这些事情。
0赞 Vincent Bénet 7/29/2020
你能解释一下它为什么有效吗?(你把我省在循环中生成的回调上,参数是循环的最后一个,所以谢谢!
67赞 Aran-Fey 3/11/2019 #2

解释

这里的问题是,创建函数时不会保存 的值。相反,查找调用时的值。iffi

如果你仔细想想,这种行为是完全有道理的。事实上,这是函数工作的唯一合理方式。假设你有一个访问全局变量的函数,如下所示:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

当你阅读这段代码时,你当然会期望它打印“bar”,而不是“foo”,因为在函数声明后,值 of 发生了变化。同样的事情也发生在你自己的代码中:当你调用时,的值已经改变并被设置为。global_varfi2

解决方案

实际上有很多方法可以解决这个问题。以下是一些选项:

  • 通过将其用作默认参数来强制早期绑定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.partialif

    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]

请注意,即使我们将其转换为默认参数,仍然发生了更改!如果你的代码发生了变异,那么你必须将 的副本绑定到你的函数,如下所示:iii

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())

评论

2赞 Karl Knechtel 8/19/2022
原始代码已经使用了闭包 - 并且闭包仅由 Python 在幕后创建。只是在 OP 的循环中创建的每个函数,都从同一个本地命名空间生成其闭包;而每次调用 to 都会创建一个具有新局部变量的新堆栈帧,每个闭包将单独使用这些变量。我们仍然可以在创建后(但在返回之前)修改 *within。f_factoryif_factoryf
0赞 DMeneses 2/7/2021 #3

为了补充 @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))
-1赞 Martin Theodorus Mathew 1/4/2022 #4

你可以像这样尝试:

l=[]
for t in range(10):
    def up(y):
        print(y)
    l.append(up)
l[5]('printing in 5th function')

评论

2赞 Community 1/4/2022
您的答案可以通过其他支持信息进行改进。请编辑以添加更多详细信息,例如引文或文档,以便其他人可以确认您的答案是正确的。您可以在帮助中心找到有关如何写出好答案的更多信息。
-4赞 Chunie 10/11/2022 #5

只需修改最后一行

functions.append(f())

编辑:这是因为是一个函数 - python 将函数视为一等公民,您可以在变量中传递它们以供以后调用。所以你的原始代码正在做的是将函数本身追加到列表中,而你要做的是将函数的结果追加到列表中,这就是上面的行通过调用函数来实现的。f

评论

1赞 Sören 10/17/2022
OP 想要创建一个函数列表。不是数字列表。
-1赞 K4liber 11/29/2022 #6

您必须将每个值保存在内存中的单独空间中,例如: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]
2赞 Basj 6/7/2023 #7

对于那些使用以下方法回答这个问题的人:lambda

解决方案是简单地替换为 。lambda: ilambda 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 函数立即(而不是推迟)评估变量