为什么map()和列表推导的结果不同?

12 投票
3 回答
2714 浏览
提问于 2025-04-11 09:23

下面这个测试没有通过:

#!/usr/bin/env python
def f(*args):
    """
    >>> t = 1, -1
    >>> f(*map(lambda i: lambda: i, t))
    [1, -1]
    >>> f(*(lambda: i for i in t)) # -> [-1, -1]
    [1, -1]
    >>> f(*[lambda: i for i in t]) # -> [-1, -1]
    [1, -1]
    """
    alist = [a() for a in args]
    print(alist)

if __name__ == '__main__':
    import doctest; doctest.testmod()

换句话说:

>>> t = 1, -1
>>> args = []
>>> for i in t:
...   args.append(lambda: i)
...
>>> map(lambda a: a(), args)
[-1, -1]
>>> args = []
>>> for i in t:
...   args.append((lambda i: lambda: i)(i))
...
>>> map(lambda a: a(), args)
[1, -1]
>>> args = []
>>> for i in t:
...   args.append(lambda i=i: i)
...
>>> map(lambda a: a(), args)
[1, -1]

3 个回答

4

表达式 f = lambda: i 的意思是:

def f():
    return i

表达式 g = lambda i=i: i 的意思是:

def g(i=i):
    return i

在第一个例子中,i 是一个 自由变量,也就是说它没有被限制在某个特定的地方。而在第二个例子中,i 是函数的参数,也就是一个局部变量。默认参数的值是在定义函数的时候就确定下来的。

生成器表达式是最近的封闭作用域(也就是i 被定义的地方),所以在 lambda 表达式中,i 是在那个块里被解析的:

f(*(lambda: i for i in (1, -1)) # -> [-1, -1]

lambda i: ... 这个块中,i 是一个局部变量,因此它所指向的对象是在这个块里定义的:

f(*map(lambda i: lambda: i, (1,-1))) # -> [1, -1]
6

这个 lambda 表达式捕获的是变量,而不是它的值,所以这段代码

lambda : i

在调用的时候,始终会返回变量 i 当前 绑定的值。到它被调用时,这个值已经被设定为 -1 了。

如果你想得到你想要的结果,你需要在创建 lambda 表达式的时候捕获当时的绑定值,可以通过以下方式实现:

>>> f(*(lambda i=i: i for i in t)) # -> [-1, -1]
[1, -1]
>>> f(*[lambda i=i: i for i in t]) # -> [-1, -1]
[1, -1]
9

它们是不同的,因为在生成器表达式和列表推导式中,i 的值是懒惰求值的,也就是说,只有在匿名函数被调用时,i 的值才会被计算出来。
到那时,i 已经绑定到了最后一个值,也就是 -1。

基本上,这就是列表推导式的工作原理(生成器表达式也是一样的):

x = []
i = 1 # 1. from t
x.append(lambda: i)
i = -1 # 2. from t
x.append(lambda: i)

现在,这些匿名函数(也叫 lambda 函数)携带着一个闭包,引用了 i,但在这两种情况下,i 都绑定到了 -1,因为这是它最后被赋值的结果。

如果你想确保 lambda 函数接收到当前的 i 值,可以这样做:

f(*[lambda u=i: u for i in t])

这样,你就强制在创建闭包时计算 i 的值。

编辑:生成器表达式和列表推导式之间有一个区别:后者会把循环变量泄露到外部作用域。

撰写回答