【问题标题】:Lambdas from a list comprehension are returning a lambda when called列表推导中的 Lambda 在调用时返回一个 lambda
【发布时间】:2016-07-14 08:43:46
【问题描述】:

我正在尝试在test.py 中的列表上迭代 lambda func,并且我想获取 lambda 的调用结果,而不是函数对象本身。但是,以下输出确实让我感到困惑。

------test.py---------
#!/bin/env python
#coding: utf-8

a = [lambda: i for i in range(5)]
for i in a:
    print i()

--------output---------
<function <lambda> at 0x7f489e542e60>
<function <lambda> at 0x7f489e542ed8>
<function <lambda> at 0x7f489e542f50>
<function <lambda> at 0x7f489e54a050>
<function <lambda> at 0x7f489e54a0c8>

我在将调用结果打印到t时修改了变量名,如下所示,一切顺利。我想知道这是怎么回事。 ?

--------test.py(update)--------
a = [lambda: i for i in range(5)]
for t in a:
    print t()

-----------output-------------
4
4
4
4
4

【问题讨论】:

  • 抱歉结束,我看错了问题。
  • 哎呀,抱歉举报了。我也看错了!
  • 我更改了标题,因为它真的很容易略过您的问题并认为您遇到了一个简单的后期绑定问题,而不是与重用变量名相关的问题。 (这在错误的闭包和答案中得到了证明。)新标题引起了人们对您所关注的真正问题的关注。如果您想根据我的做法改写它,请确保保留该方面。
  • 以上礼貌和谦逊的表现是我喜欢 Python 社区的地方,尤其是那些经常使用 SO 的 Python 达人。

标签: python lambda


【解决方案1】:

在 Python 2 中,列表解析将变量“泄漏”到外部范围:

>>> [i for i in xrange(3)]
[0, 1, 2]
>>> i
2

请注意,Python 3 上的行为有所不同:

>>> [i for i in range(3)]
[0, 1, 2]
>>> i
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined

当您定义 lambda 时,它绑定到变量 i,而不是第二个示例所示的当前值。 现在,当您为 i 分配新值时,lambda 将返回当前值:

>>> a = [lambda: i for i in range(5)]
>>> a[0]()
4
>>> i = 'foobar'
>>> a[0]()
'foobar'

由于循环中 i 的值是 lambda 本身,因此您可以将其作为返回值:

>>> i = a[0]
>>> i()
<function <lambda> at 0x01D689F0>
>>> i()()()()
<function <lambda> at 0x01D689F0>

更新:Python 2.7 示例:

Python 2.7.6 (default, Jun 22 2015, 17:58:13) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> a = [lambda: i for i in range(5)]
>>> for i in a:
...     print i()
... 
<function <lambda> at 0x7f1eae7f15f0>
<function <lambda> at 0x7f1eae7f1668>
<function <lambda> at 0x7f1eae7f16e0>
<function <lambda> at 0x7f1eae7f1758>
<function <lambda> at 0x7f1eae7f17d0>

在 Python 3.4 上也是如此:

Python 3.4.3 (default, Oct 14 2015, 20:28:29) 
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = [lambda: i for i in range(5)]
>>> for i in a:
...     print(i())
... 
4
4
4
4
4

有关列表理解的变量范围更改的详细信息,请参阅 Guido 的 blogpost from 2010

我们还在 Python 3 中进行了另一项更改,以提高列表推导式和生成器表达式之间的等价性。在 Python 2 中,列表推导将循环控制变量“泄漏”到周围的作用域中:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

然而,在 Python 3 中,我们决定使用与生成器表达式相同的实现策略来修复列表推导的“肮脏小秘密”。因此,在 Python 3 中,上面的示例(修改为使用 print(x) :-) 将打印 'before',证明列表推导中的 'x' 暂时隐藏但不会覆盖周围的 'x'范围。

【讨论】:

  • python 2 中列表解析的泄漏范围不会影响 OP 中的行为。
  • @juanpa.arrivillaga 您是否尝试过在两个版本中运行有问题的代码?至少我在 Python 2.7 和 3.4 中得到了不同的结果。
  • 哎呀!我完全误读了这个问题!你完全正确!这对我来说是令人惊讶的行为......
  • 转念一想,这并不令人惊讶的行为。
  • 非常感谢您的详细解释,我明白了! ;)
【解决方案2】:

Python 中的闭包是late-binding,这意味着列表中的每个 lambda 函数只会在调用时评估变量 i,而在定义时不会。这就是为什么所有函数都返回相同的值,即 ì 的最后一个值(即 4)。

为避免这种情况,一种技术是将i 的值绑定到本地命名参数:

>>> a = [lambda i=i: i for i in range(5)]
>>> for t in a:
...   print t()
... 
0
1
2
3
4

另一种选择是创建partial function 并将i 的当前值绑定为其参数:

>>> from functools import partial
>>> a = [partial(lambda x: x, i) for i in range(5)]
>>> for t in a:
...   print t()
... 
0
1
2
3
4

编辑:抱歉,最初误读了问题,因为这类问题经常与后期绑定有关(感谢@soon 的评论)。

该行为的第二个原因是列表解析的变量在 Python2 中泄漏,正如其他人已经解释的那样。当在for 循环中使用i 作为迭代变量时,每个函数都会打印i 的当前值(出于上述原因),这只是函数本身。 当使用不同的名称(例如 t)时,函数会打印 i 的最后一个值,就像它在列表理解循环中一样,即 4。

【讨论】:

  • 好吧,但是为什么在for循环中改变变量名会改变输出呢?
  • @soon 你指的是什么变量,t?这不会影响任何事情,它只是我们在迭代列表时为每个函数提供的临时名称。还是我误解了这个问题?
  • 我对这个问题的解释是“为什么 for i in a 打印 lambdas,但 for k in a 打印数字?”。当然,这也是因为后期绑定,但这对于 OP 来说可能还不清楚。无论如何都要 +1
  • 啊,我明白了,我可能是太仓促了。 :) 这是因为 Python 2 中的变量泄漏,正如其他人已经解释的那样。
【解决方案3】:

lambda: i 是一个匿名函数,没有返回 i 的参数。因此,您正在生成一个匿名函数列表,您可以稍后(在第二个示例中)将其绑定到名称 t 并使用 () 调用。请注意,您可以对非匿名函数执行相同操作:

>>> def f():
...   return 42
... 
>>> name = f
>>> name
<function f at 0x7fed4d70fb90>
>>> name()
42

@plamut 刚刚回答了问题的隐含的其他部分,所以我不会。

【讨论】:

    猜你喜欢
    • 2015-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多