【问题标题】:Lambdas inside list comprehensions列表推导中的 Lambdas
【发布时间】:2012-04-21 15:57:21
【问题描述】:

我想要一个 lambdas 列表,作为一些繁重计算的缓存,并注意到这一点:

>>> [j() for j in [lambda:i for i in range(10)]]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]

虽然

>>> list([lambda:i for i in range(10)])
[<function <lambda> at 0xb6f9d1ec>, <function <lambda> at 0xb6f9d22c>, <function <lambda> at 0xb6f9d26c>, <function <lambda> at 0xb6f9d2ac>, <function <lambda> at 0xb6f9d2ec>, <function <lambda> at 0xb6f9d32c>, <function <lambda> at 0xb6f9d36c>, <function <lambda> at 0xb6f9d3ac>, <function <lambda> at 0xb6f9d3ec>, <function <lambda> at 0xb6f9d42c>]

这意味着 lambda 是唯一的函数,但它们都共享相同的索引值。

这是错误还是功能?我该如何避免这个问题?它不仅限于列表推导......

>>> funcs = []
... for i in range(10):
...     funcs.append(lambda:i)
... [j() for j in funcs]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]

【问题讨论】:

    标签: python lambda functional-programming list-comprehension


    【解决方案1】:

    将方括号变成括号,如下所示: [j() for j in (lambda:i for i in range(10))] 输出: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 元组是不可变的变量

    【讨论】:

      【解决方案2】:

      您在这里看到的是closures 的效果。 lambda 正在从程序中捕获状态以供以后使用。因此,虽然每个 lambda 都是唯一的对象,但状态不一定是唯一的。

      这里实际的“gotchya”是变量i 被捕获,而不是i 在那个时间点表示的值。我们可以用一个更简单的例子来说明这一点:

      >>> y = 3
      >>> f = lambda: y
      >>> f()
      3
      >>> y = 4
      >>> f()
      4
      

      lambda 保持对变量的引用,并在您执行 lambda 时评估该变量。

      要解决这个问题,您可以在 lambda 中分配一个局部变量:

      >>> f = lambda y=y:y
      >>> f()
      4
      >>> y = 6
      >>> f()
      4
      

      最后,在循环的情况下,循环变量只被“声明”一次。因此,在循环中对循环变量的任何引用都将持续到下一次迭代之后。这包括列表推导中的变量。

      【讨论】:

      • 嗯,任何 lambda 函数都是闭包。我不认为张贴者对关闭是什么感到困惑。出乎意料的是,在这种情况下它使用了变量引用而不是值。除非使用引用类型,否则大多数使用闭包的语言都会在这种情况下替换该值。
      • @KeithIrwin,大多数允许闭包的语言都受到这种确切的“gotchya”的影响。 Javascript 和 C# 是两个值得注意的例子。另外,OP 从来没有提到闭包这个词,所以我想如果他们不知道,我会链接到解释它的材料。
      • 它只出现在没有明确区分值和引用的语言中。例如,它出现在 Lisp 函数式语言家族中,但没有出现在像 ML 家族、Haskell 或 Scala 这样的强类型语言中。如果他一直在使用 F#、Ocaml、Haskell 或 Scala 等语言工作,他会合理地期待完全相反的行为。我认为他至少有一些函数式语言的经验,因为他将这篇文章标记为关于函数式编程。
      【解决方案3】:

      lambda 在您调用它时返回 i 的值。由于您在循环运行完成后调用lambda,因此i 的值将始终为9。

      您可以在 lambda 中创建一个本地 i 变量来保存定义 lambda 时的值:

      >>> [j() for j in [lambda i=i:i for i in range(10)]]
      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      

      另一种解决方案是创建一个返回lambda的函数:

      def create_lambda(i):
          return lambda:i
      >>> [j() for j in [create_lambda(i) for i in range(10)]]
      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      

      之所以有效,是因为每次调用 create_lambda 都会创建一个不同的闭包(持有不同的 i 值)。

      【讨论】:

      • 当您希望您的lambda 支持*args 时,第一种方法效果不佳。第二种方法将在那里工作,但......更多的工作:)
      • 太糟糕了,我不能给两个答案都打上绿色标记。我选择了这个,因为它实际上给出了正确的代码来剪切和粘贴,就像我五岁一样,我喜欢这种方法来回答 SO。
      • 对于我们这些不太擅长 Python 的人,您是否有可能对第一种方法中的一个实例使用与 i 不同的变量?
      • @Chowlett:我更喜欢在上面的示例中使用i,因为使用相同的变量名是常见的习惯用法。你也可以使用[lambda k=i:k for i in range(10)] 来做同样的事情。
      • 干杯。如果我熟悉语法,我想我也会这样做;但是使用 k 可以让我看到哪个字母映射到 in-lambda 变量以及哪个字母映射到理解变量。
      【解决方案4】:

      问题是您没有在列表理解的每次迭代中捕获 i 的值,而是每次都在捕获变量。

      问题在于闭包通过引用捕获变量。在这种情况下,您正在捕获一个其值随时间变化的变量(与所有循环变量一样),因此运行它时它的值与创建它时的值不同。

      【讨论】:

        【解决方案5】:

        我不确定这是错误还是功能,但发生的情况是lambda:i 在形成 lambda 函数之前没有评估 i。因此,它实际上只是一个评估 i 的当前值的函数。这是如何发生的另一个示例。

        >>> i=5
        >>> x=lambda:i
        >>> x()
        5
        >>> i=6
        >>> x()
        6
        

        所以,很明显,除了 i 在您的示例中将变为 9 之外,发生的事情是相同的,因为它是按该顺序在 0 到 9 的范围内分配的。

        我认为没有什么好的方法可以避免它。 Python 中的 Lambda 函数非常有限。它本质上并不是一种真正的函数式语言。

        【讨论】:

          猜你喜欢
          • 2018-10-30
          • 2020-04-07
          • 2012-05-04
          • 2016-02-13
          • 2017-10-09
          • 2017-12-23
          • 1970-01-01
          • 1970-01-01
          • 2011-07-23
          相关资源
          最近更新 更多