循环创建函数


102

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

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


Answers:


167

您在后期绑定方面遇到了麻烦 -每个函数都i尽可能晚地查找(因此,在循环结束后调用时,i将设置为2)。

可以通过强制早期绑定轻松解决:更改def f():def f(i=i):

def f(i=i):
    return i

缺省值(右手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语句。


7
您怎么知道如何解决这些问题?
alwbtc

3
@alwbtc主要是经验,大多数人有时会自己面对这些事情。
ruohola

您能解释一下为什么它起作用吗?(您将我保存在循环中生成的回调中,参数始终处于循环的最后状态,所以谢谢!)
VincentBénet

20

说明

这里的问题是创建i函数时未保存的值f。而是查询何时调用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绑定的当前值if

    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())
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.