繁体   English   中英

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

[英]Weird behavior: Lambda inside list comprehension

在python 2.6中:

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

结果是:

[3, 3, 3]

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

我错过了什么?

为了让 lambda 记住m的值,你可以使用一个带有默认值的参数:

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

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

您遇到的效果称为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,并且所有三个函数都看到相同的值。

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

-- 评论区 --

>>> [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。

查看函数的__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]

就个人而言,我发现这是一个更优雅的解决方案。 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 中不同:这两个列表推导式在性能上更接近并且都慢了很多,map 仍然更快。)

@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 -> ... }而不是 while 循环,则不会发生这种情况。 Groovy while 循环和 Python 列表推导式在迭代之间共享其闭包。

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

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

[注意:这个答案是不正确的; 见评论]

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM