【问题标题】:Lambda function in list comprehensions列表推导中的 Lambda 函数
【发布时间】:2011-05-20 18:39:00
【问题描述】:

即使flambda 函数相同,为什么以下两个列表推导的输出却不同?

f = lambda x: x*x
[f(x) for x in range(10)]

[lambda x: x*x for x in range(10)]

请注意,type(f)type(lambda x: x*x) 返回相同的类型。

【问题讨论】:

  • [lambda x: x*x for x in range(10)] 比第一个更快,因为它不会重复调用外部循环函数 f。
  • @Selinap: ...不,相反,您每次都在循环中创建一个全新的功能。 ...以及创建这个新函数的开销,然后调用会慢一些(无论如何在我的系统上)。
  • @Gerrat:即使有开销,它仍然更快。但是,[x*x for x in range(10)] 当然更好。
  • 我刚进入这里是为了获取 google foobar 访问权限 :)

标签: python


【解决方案1】:

第一个创建一个 lambda 函数并调用它十次。

第二个不调用该函数。它创建了 10 个不同的 lambda 函数。它将所有这些都放在一个列表中。要使其与您需要的第一个等效:

[(lambda x: x*x)(x) for x in range(10)]

或者更好:

[x*x for x in range(10)]

【讨论】:

  • map(lambda x: x*x, range(10)),这可能是OP最初的意思。
  • 是的,lambda x : x*x .. (x) 似乎很重要。
  • [lambda x: x*x for x in range(10)] 基本上是haskell中的一个函数
  • @DanielRoseman,或者更准确地说是list(map(lambda x: x*x, range(10))) 会给你[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
【解决方案2】:

这个问题涉及到“著名”和“显而易见”的 Python 语法中一个非常臭的部分——什么是优先的、lambda 或 for of list comprehension。

我不认为 OP 的目的是生成从 0 到 9 的正方形列表。如果是这样,我们可以提供更多解决方案:

squares = []
for x in range(10): squares.append(x*x)
  • 这是命令式语法的好方法。

但这不是重点。关键是 W(hy)TF 这个模棱两可的表达是否如此违反直觉?最后我有一个愚蠢的案例给你,所以不要太早驳回我的回答(我在求职面试中得到了它)。

所以,OP 的理解返回了一个 lambda 列表:

[(lambda x: x*x) for x in range(10)]

这当然只是平方函数的 10 个不同副本,请参阅:

>>> [lambda x: x*x for _ in range(3)]
[<function <lambda> at 0x00000000023AD438>, <function <lambda> at 0x00000000023AD4A8>, <function <lambda> at 0x00000000023AD3C8>]

注意 lambda 的内存地址 - 它们都是不同的!

你当然可以有这个表达式的更“最佳”(哈哈)版本:

>>> [lambda x: x*x] * 3
[<function <lambda> at 0x00000000023AD2E8>, <function <lambda> at 0x00000000023AD2E8>, <function <lambda> at 0x00000000023AD2E8>]

看到了吗? 3 次相同 lambda。

请注意,我使用_ 作为for 变量。它与lambda 中的x 无关(它在词汇上被掩盖了!)。明白了吗?

我不讨论了,为什么语法优先级不是这样,这一切都意味着:

[lambda x: (x*x for x in range(10))]

可能是:[[0, 1, 4, ..., 81]],或[(0, 1, 4, ..., 81)],或我认为最合乎逻辑的,这将是 1 个元素的 list - 返回值的 generator。事实并非如此,语言不是这样工作的。

但是什么,如果...

如果你不掩盖for 变量,并且在你的lambdas 中使用它呢???

好吧,那么废话就发生了。看看这个:

[lambda x: x * i for i in range(4)]

这当然意味着:

[(lambda x: x * i) for i in range(4)]

但这并不意味着:

[(lambda x: x * 0), (lambda x: x * 1), ... (lambda x: x * 3)]

这太疯狂了!

列表推导中的 lambda 是此推导范围的闭包。 lexical 闭包,因此它们通过引用引用 i,而不是在评估它们时的值!

所以,这个表达式:

[(lambda x: x * i) for i in range(4)]

大致等价于:

[(lambda x: x * 3), (lambda x: x * 3), ... (lambda x: x * 3)]

我确信我们可以在这里使用 python 反编译器看到更多信息(我的意思是,例如 dis 模块),但对于 Python-VM 不可知论的讨论,这已经足够了。 面试问题就这么多。

现在,如何制作乘数 lambda 的 list,它真正乘以连续整数?好吧,与公认的答案类似,我们需要打破与i 的直接联系,方法是将其包装在另一个lambda 中,该lambda 被称为inside列表理解表达式:

之前:

>>> a = [(lambda x: x * i) for i in (1, 2)]
>>> a[1](1)
2
>>> a[0](1)
2

之后:

>>> a = [(lambda y: (lambda x: y * x))(i) for i in (1, 2)]
>>> a[1](1)
2
>>> a[0](1)
1

(我有外部 lambda 变量也 = i,但我认为这是更清晰的解决方案 - 我引入了 y 以便我们都可以看到哪个女巫是哪个)。

编辑 2019-08-30:

遵循@josoler 的建议,@sheridp 的回答中也有该建议 - 列表推导“循环变量”的值可以“嵌入”到对象中 - 关键是它可以在合适的时间。上面的“After”部分通过将其包装在另一个lambda 中并立即使用i 的当前值调用它。另一种方法(更容易阅读 - 它不会产生“WAT”效果)是将i 的值存储在partial 对象中,并让“内部”(原始)lambda 将其作为参数(在调用时由partial 对象提供),即:

2 之后:

>>> from functools import partial
>>> a = [partial(lambda y, x: y * x, i) for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

太棒了,但对你来说还有一点点转折!假设我们不想让代码阅读器更容易,而是按名称传递因子(作为partial 的关键字参数)。让我们做一些重命名:

2.5 之后:

>>> a = [partial(lambda coef, x: coef * x, coef=i) for i in (1, 2)]
>>> a[0](1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() got multiple values for argument 'coef'

什么?

>>> a[0]()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'x'

等等...我们正在将参数的数量减 1,然后从“太多”变为“太少”?

好吧,它不是真正的 WAT,当我们以这种方式将 coef 传递给 partial 时,它就变成了关键字参数,所以它必须在位置 x 参数之后,如下所示:

3 之后:

>>> a = [partial(lambda x, coef: coef * x, coef=i) for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

我更喜欢最后一个版本而不是嵌套的 lambda,但每个版本都有自己的...

编辑 2020-08-18:

感谢评论者 dasWesen,我发现 Python 文档中涵盖了这些内容:https://docs.python.org/3.4/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result - 它处理循环而不是列表推导,但想法是相同的 - lambda 函数中的全局或非局部变量访问.甚至还有一个解决方案 - 使用默认参数值(就像任何函数一样):

>>> a = [lambda x, coef=i: coef * x for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

这样,coef 值在函数定义时绑定到 i 的值(请参阅 James Powell 的演讲“从上到下,从左到右”,这也解释了为什么回避可变默认值)。

【讨论】:

  • 这是一个残酷而又不寻常的求职面试问题。
  • 如果我的同事不问,我可能永远不会搜索这个答案
  • 哇。我只是被这种荒谬的行为咬得很厉害。谢谢你的帖子!
  • 为了清楚和完整起见,您可以编写最后一个列表理解,如:[partial(lambda i, x: i * x, i) for i in (1, 2)]
  • 出于多种目的,我认为有一种 python 内部的方法可以解决它:具有默认值的 Lambda。这应该工作:[lambda x, i=i: x * i for i in range(4)]docs.python.org/3.4/faq/…。如果您不介意额外的可覆盖参数,那就太好了。
【解决方案3】:

最大的不同是第一个示例实际上调用了 lambda f(x),而第二个示例没有。

您的第一个示例等效于[(lambda x: x*x)(x) for x in range(10)],而您的第二个示例等效于[f for x in range(10)]

【讨论】:

    【解决方案4】:

    第一个

    f = lambda x: x*x
    [f(x) for x in range(10)]
    

    为范围内的每个值运行f(),因此它为每个值运行f(x)

    第二个

    [lambda x: x*x for x in range(10)]
    

    为列表中的每个值运行 lambda,因此它会生成所有这些函数。

    【讨论】:

      【解决方案5】:

      人们给出了很好的答案,但忘记提及我认为最重要的部分: 在第二个示例中,列表推导的Xlambda 函数的X 不同,它们完全不相关。 所以第二个例子其实是一样的:

      [Lambda X: X*X for I in range(10)]
      

      range(10) 上的内部迭代仅负责在列表中创建 10 个相似的 lambda 函数(10 个单独的函数但完全相似 - 返回每个输入的 2 次方)。

      另一方面,第一个示例的工作方式完全不同,因为迭代的 X 确实与结果交互,每次迭代的值是 X*X,所以结果将是 [0,1,4,9,16,25, 36, 49, 64 ,81]

      【讨论】:

      • 这是很重要的一点。我对你投了赞成票,并在我的回答中详细说明了这一点。
      【解决方案6】:

      其他答案是正确的,但如果您尝试制作一个函数列表,每个函数都有不同的参数,可以在稍后执行,下面的代码将执行此操作:

      import functools
      a = [functools.partial(lambda x: x*x, x) for x in range(10)]
      
      b = []
      for i in a:
          b.append(i())
      
      In [26]: b
      Out[26]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
      

      虽然这个例子是人为设计的,但当我想要一个函数列表时,我发现它很有用,每个函数都打印不同的东西,即

      import functools
      a = [functools.partial(lambda x: print(x), x) for x in range(10)]
      
      for i in a:
          i()
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-07-24
        • 2018-08-27
        • 1970-01-01
        • 2022-11-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多