【问题标题】:Creating functions in a loop在循环中创建函数
【发布时间】:2018-11-16 17:38:22
【问题描述】:

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

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

【问题讨论】:

标签: python function


【解决方案1】:

您遇到了延迟绑定的问题——每个函数都尽可能晚地查找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 语句。

【讨论】:

  • 你怎么知道如何解决这些问题?
  • @alwbtc 这主要是经验,大多数人在某个时候都自己面对过这些事情。
  • 你能解释一下为什么它可以工作吗? (你救了我在循环中生成的回调,参数总是循环的最后,所以谢谢!)
【解决方案2】:

解释

这里的问题是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.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())

【讨论】:

    【解决方案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))
    

    【讨论】:

      【解决方案4】:

      你可以这样试试:

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

      【讨论】:

      • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-21
      相关资源
      最近更新 更多