为什么map()和列表推导的结果不同?
下面这个测试没有通过:
#!/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
的值。
编辑:生成器表达式和列表推导式之间有一个区别:后者会把循环变量泄露到外部作用域。