关于python:在循环中创建函数

Creating functions in a loop

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

1
2
3
4
5
6
7
8
9
functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

问题是所有功能最终都是相同的。这三个函数都返回2,而不是返回0、1和2:

1
2
3
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):,如下所示:

1
2
def f(i=i):
    return i

默认值(i=i中的右手边i是参数名i的默认值,它是i=i中的左手边i被查找到def时间,而不是call时间,因此本质上它们是一种专门寻找早期绑定的方法。

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

1
2
3
4
def make_f(i):
    def f():
        return i
    return f

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


解释

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

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

1
2
3
4
5
6
7
global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

当您阅读此代码时,您当然希望它打印"bar",而不是"foo",因为在声明函数后EDOCX1的值(19)发生了更改。在您自己的代码中也发生了同样的事情:当您调用f时,i的值已经更改并设置为2

解决方案

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

  • 强制早期绑定i,将其用作默认参数

    与闭包变量(如i)不同,在定义函数时,将立即计算默认参数:

    1
    2
    3
    4
    5
    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i

        functions.append(f)

    为了深入了解这是如何工作的/为什么工作的:函数的默认参数存储为函数的一个属性;因此,i的当前值被快照并保存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> 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是一个可以改变的变量。我们可以通过创建另一个保证永远不会改变的变量来解决这个问题,最简单的方法是关闭:

    1
    2
    3
    4
    5
    6
    7
    8
    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允许将参数附加到现有函数。在某种程度上,它也是一种功能工厂。

    1
    2
    3
    4
    5
    6
    7
    8
    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)

警告:这些解决方案仅在为变量指定新值时有效。如果修改存储在变量中的对象,将再次遇到相同的问题:

1
2
3
4
5
6
7
>>> 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的副本绑定到您的函数,如下所示:

  • 埃多克斯1〔35〕
  • 埃多克斯1〔36〕
  • 埃多克斯1〔37〕