在循环中创建函数

时间:2010-08-07 19:04:34

标签: python function

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

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种不同功能?

3 个答案:

答案 0 :(得分:119)

你遇到了后期绑定的问题 - 每个函数尽可能晚地查找i(因此,在循环结束后调用{{1} }将设置为i)。

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

def f(i=i):

默认值(def f(i=i): return i 中的右侧i是参数名称i=i的默认值,ii中的左侧i=idef时间查询,而不是call时间,所以基本上它们是专门寻找早期约束的一种方式。

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

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

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

答案 1 :(得分:11)

说明

这里的问题是创建函数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 current 值将被快照并保存。

    >>> 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是可以更改的变量。我们可以通过创建保证永不更改的另一个变量来解决此问题,而最简单的方法是 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)
    
  • 使用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())

答案 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))