列表推导式中的Lambda函数

为什么下面两个列表推导式的输出不同,即使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)返回相同的类型。

537662 次浏览

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

第二个不调用函数。它创建了10个不同的函数。它把所有这些都放在一个列表中。为了使它与第一个等价,你需要:

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

或者更好的是:

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

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

第一个例子等价于[(lambda x: x*x)(x) for x in range(10)],而第二个例子等价于[f for x in range(10)]

第一个

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,因此它生成所有这些函数。

人们给出了很好的答案,但忘记了我认为最重要的部分: 在第二个例子中,列表推导式的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]

这个问题触及了“著名”的一个非常糟糕的部分。和“;obvious"Python语法-列表推导式的lambda或for优先级。

我不认为OP的目的是生成一个从0到9的正方形列表。如果是这样的话,我们可以给出更多的解:

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

但这不是重点。关键是W(为什么)TF这个模糊的表达式如此反直觉?最后我有一个白痴的案例要告诉你,所以不要过早忽略我的回答(我在一次工作面试中提到过)。

因此,OP的理解返回了lambdas的列表:

[(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>]

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

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

>>> [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是该推导式范围上的闭包。一个词汇闭包,所以它们通过引用引用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不可知的讨论,这就足够了。

.

.

现在,如何创建乘数lambdas的list,它实际上与连续整数相乘?好吧,与公认的答案类似,我们需要通过将它包装在另一个lambda中来打破与i的直接联系,该lambda被称为内部,即列表理解表达式:

之前:

>>> 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对象中,并使用"inner"(原始)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的演讲“从上到下,从左到右”,这也解释了为什么避免可变默认值)。

其他答案都是正确的,但如果你试图创建一个函数列表,每个函数都有不同的参数,可以执行晚些时候,下面的代码将会做到这一点:

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()