奇怪的行为:列表理解中的 Lambda

Posted

技术标签:

【中文标题】奇怪的行为:列表理解中的 Lambda【英文标题】:Weird behavior: Lambda inside list comprehension 【发布时间】:2011-11-14 04:11:10 【问题描述】:

在 python 2.6 中:

[x() for x in [lambda: m for m in [1,2,3]]]

结果:

[3, 3, 3]

我希望输出为 [1, 2, 3]。即使使用非列表理解方法,我也会遇到完全相同的问题。甚至在我将 m 复制到另一个变量之后。

我错过了什么?

【问题讨论】:

... 但这适用于迭代器。>>> l = (lambda: m for m in [1,2,3]) >>> [x() for x in l] 这是因为生成器不会一次创建所有值,它会在请求时创建它们。列表推导式和生成器表达式并不相同,尽管它们通常可以互换使用。在某些情况下(例如这种情况),行为显着不同。 为什么x() 不只是x ??有什么不同?? @amyassin - 因为在这种情况下 x 是一个 lambda(动态声明的匿名函数)。他正在调用x() 来调用它。真的,你应该问自己的问题。 @g.d.d.c thanx,我需要知道在搜索中应该转向哪里...... 【参考方案1】:

要让 lambda 记住 m 的值,您可以使用具有默认值的参数:

[x() for x in [lambda m=m: m for m in [1,2,3]]]
# [1, 2, 3]

这是因为默认值在定义时设置一次。现在,每个 lambda 都使用自己的默认值 m,而不是在 lambda 执行时在外部范围内查找 m 的值。

【讨论】:

太棒了!没有想过为 lambda 设置默认值。很棒的帖子。【参考方案2】:

您遇到的效果称为closures,当您定义一个引用非局部变量的函数时,该函数会保留对该变量的引用,而不是获取自己的副本。为了说明,我会将您的代码扩展为不带推导式或 lambda 的等效版本。

inner_list = []
for m in [1, 2, 3]:
    def Lambda():
         return m
    inner_list.append(Lambda)

因此,此时inner_list 中包含三个函数,每个函数在调用时都会返回m 的值。但重点是他们看到的都是同一个m,尽管m 正在改变,但他们在很久以后才看到它。

outer_list = []
for x in inner_list:
    outer_list.append(x())

特别是,由于内部列表是在外部列表开始构建之前完全构建的,m 已经达到了它的最后一个值 3,并且所有三个函数都看到了相同的值。

【讨论】:

【参考方案3】:

长话短说,你不想这样做。更具体地说,您遇到的是操作顺序问题。您正在创建三个单独的lambda,它们都返回m,但它们都不会立即被调用。然后,当你到达外部列表推导并且它们都被称为m 的残差值为 3 时,内部列表推导的最后一个值。

-- 对于cmets--

>>> [lambda: m for m in range(3)]
[<function <lambda> at 0x021EA230>, <function <lambda> at 0x021EA1F0>, <function <lambda> at 0x021EA270>]

这是三个独立的 lambda。

而且,作为进一步的证据:

>>> [id(m) for m in [lambda: m for m in range(3)]]
[35563248, 35563184, 35563312]

同样,三个独立的 ID。

【讨论】:

检查 lambda 的 id()。它们都是一样的。 所以这意味着 lambda 持有对 m 的引用而不是 m 的值。是否可以让 lambda 保持变量的值?谢谢! @GeneralBecos - 我不知道,不,但如果其他人确实知道一种方法并且可以将其添加为附加评论,我很乐意记下它。除非它损害您的最终结果,否则您使用生成器的方法是合理的,这将产生预期的输出:[x() for x in (lambda: m for m in [1,2,3])]【参考方案4】:

查看函数的__closure__。所有 3 都指向同一个单元格对象,该单元格对象保持对 m 从外部范围的引用:

>>> print(*[x.__closure__[0] for x in [lambda: m for m in [1,2,3]]], sep='\n')
<cell at 0x00D17610: int object at 0x1E2139A8>
<cell at 0x00D17610: int object at 0x1E2139A8>
<cell at 0x00D17610: int object at 0x1E2139A8>

如果您不希望函数将 m 作为关键字参数,根据 unubtu 的回答,您可以改为使用额外的 lambda 在每次迭代时评估 m:

>>> [x() for x in [(lambda x: lambda: x)(m) for m in [1,2,3]]]
[1, 2, 3]

【讨论】:

【参考方案5】:

就个人而言,我发现这是一个更优雅的解决方案。 Lambda 返回一个函数,所以如果我们想使用该函数,那么我们应该使用它。对 lambda 中的“匿名”变量和生成器使用相同的符号会令人困惑,因此在我的示例中,我使用不同的符号以使其更清晰。

>>> [ (lambda a:a)(i) for i in range(3)]
[0, 1, 2]
>>> 

它也更快。

>>> timeit.timeit('[(lambda a:a)(i) for i in range(10000)]',number=10000)
9.231263160705566
>>> timeit.timeit('[lambda a=i:a  for i in range(10000)]',number=10000)
11.117988109588623
>>> 

但不如地图快:

>>> timeit.timeit('map(lambda a:a,  range(10000))',number=10000)
5.746963977813721

(我不止一次运行这些测试,结果是一样的,这是在 python 2.7 中完成的,结果在 python 3 中不同:两个列表推导在性能上更接近并且都慢得多,映射仍然快得多.)

【讨论】:

【参考方案6】:

@unubtu 的回答是正确的。我在 Groovy 中使用闭包重新创建了场景。也许是说明了正在发生的事情。

这类似于[x() for x in [lambda: m for m in [1,2,3]]]

arr = []
x = 0
while (x < 3) 
  x++
  arr.add( -> x )

arr.collect  f -> f()  == [3, 3, 3]

这类似于[x() for x in [lambda m=m: m for m in [1,2,3]]]

arr = []
x = 0
while (x < 3) 
  x++
  arr.add(_x ->  -> _x (x))

arr.collect  f -> f()  == [1, 2, 3]

请注意,如果我使用 [1,2,3].each x -&gt; ... 而不是 while 循环,则不会发生这种情况。 Groovy while 循环和 Python 列表推导式都在迭代之间共享其闭包。

【讨论】:

【参考方案7】:

我也注意到了。我得出的结论是 lambda 只创建一次。所以实际上你的内部列表理解将给出 3 个与 m 的最后一个值相关的相同函数。

尝试一下并检查元素的 id()。

[注意:此答案不正确;见 cmets]

【讨论】:

lambda 不会创建一次。 [lambda: m for m in [1,2,3]] 将生成三个单独的 lambda。 是的,注意到了。抱歉 :) id 的开头和结尾对我来说看起来一样...

以上是关于奇怪的行为:列表理解中的 Lambda的主要内容,如果未能解决你的问题,请参考以下文章

列表理解中的Python奇怪行为[重复]

C ++ lambda函数作为类成员:奇怪的行为

循环中的列表行为非常奇怪

Excel中的奇怪ActiveX列表框行为

QThread 线程间通信:连接到 &QThread::quit 与连接到 lambda [&thread] thread->quit(); 的奇怪行为

用户在我的应用中滚动 UIViewCollectionCells 列表时的奇怪行为(每个单元格中的数据随机更改)