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

207

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

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?

11
作为自我提醒:http://docs.python-guide.org/en/latest/writing/gotchas/#late-binding-closures - Skiptomylu
1
请注意,如果您在迭代生成器并调用每个函数,则问题可能不会出现。这是因为所有内容都是惰性评估的,因此与绑定一样“晚”。循环的迭代变量增加,下一个函数或lambda立即创建,然后立即调用该函数或lambda - 使用当前迭代值。生成器表达式也适用于相同的情况。有关示例,请参见https://dev59.com/Gqrka4cB1Zd3GeqPb1KS。 - Karl Knechtel
解决方案:将 lambda: i 替换为 lambda i=i: i。您的代码现在是 for i in range(3): functions.append(lambda i=i: i) - Basj
解决方案:将lambda: i替换为lambda i=i: i。您的代码现在是for i in range(3): functions.append(lambda i=i: i) - undefined
7个回答

264

你遇到了一个关于延迟绑定的问题 -- 每个函数尽可能晚地查找变量i(因此,当在循环结束后调用时,i将被设为2)。

通过强制进行早期绑定来轻松解决:像这样将def f():更改为def f(i=i):

def f(i=i):
    return i

默认值(在i=i中右边的i是参数名i的默认值,而左边的i是函数定义中使用该参数的变量名)在定义函数时就会被查找,而不是在调用时,因此本质上它们是一种寻找早绑定的特殊方式。

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

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

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


16
你如何知道如何修理这些东西? - alwbtc
10
@alwbtc,这主要是通过经验学习的,大多数人在某个时候都会自己面对这些事情。 - ruohola
你能解释一下为什么它能工作吗?(你在循环中生成的回调函数中帮了我,参数总是循环的最后一个,所以谢谢你!) - Vincent Bénet

65

说明

问题在于在函数 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。
解决方案:
实际上有很多种方法可以解决这个问题,以下是一些选项:
  • Force early binding of i by using it as a default argument

    Unlike closure variables (like i), default arguments are evaluated immediately when the function is defined:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)
    

    To give a little bit of insight into how/why this works: A function's default arguments are stored as an attribute of the function; thus the current value of i is snapshotted and saved.

    >>> 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,)
    
  • Use a function factory to capture the current value of i in a closure

    The root of your problem is that i is a variable that can change. We can work around this problem by creating another variable that is guaranteed to never change - and the easiest way to do this is a closure:

    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)
    
  • Use functools.partial to bind the current value of i to f

    functools.partial lets you attach arguments to an existing function. In a way, it too is a kind of function factory.

    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())

2
原始代码已经使用了闭包 - 而且这些闭包只是由Python在幕后创建的。只是在OP中循环创建的每个函数,都从相同的本地命名空间生成其闭包;而每次调用f_factory都会创建一个新的堆栈帧和新的本地变量,每个闭包将单独使用它们。我们仍然可以在创建(但在返回)f之后在f_factory内修改i - Karl Knechtel

2

0
为了补充 @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

你必须将每个 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]

-1
你可以尝试这样做:
l=[]
for t in range(10):
    def up(y):
        print(y)
    l.append(up)
l[5]('printing in 5th function')

2
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

-4

只需修改最后一行

functions.append(f())

编辑:这是因为f是一个函数——Python将函数视为一等公民,您可以将它们传递到变量中以便稍后调用。因此,您原始的代码所做的是将函数本身附加到列表中,而您想要做的是将函数的结果附加到列表中,这就是上面一行通过调用函数实现的内容。

1
OP想要创建一个函数列表,而不是数字列表。 - Sören

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接