列表推导与map比较
有没有什么理由让我们更喜欢用 map() 而不是列表推导式,或者反过来呢?这两者在效率上有没有谁更好,或者说哪种写法更符合 Python 的风格呢?
14 个回答
Python 2:你应该使用 map
和 filter
,而不是列表推导式。
一个客观的理由是,尽管它们看起来不太“Python风格”,但你还是应该更倾向于使用它们:
因为它们需要函数或匿名函数作为参数,这引入了新的作用域。
我曾经因此吃过亏不止一次:
for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!
但如果我当时说的是:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
那么一切就会没问题。
你可能会说我在同一个作用域里用同样的变量名太傻了。
其实我并没有。最开始的代码是没问题的——那两个 x
并不在同一个作用域里。
问题出现在我把内部代码块移动到代码的其他部分后(注意:这是维护时的问题,不是开发时的问题),而我没有预料到这一点。
是的,如果你从来不犯这个错误,那么列表推导式确实更优雅。
但根据我的个人经验(以及看到其他人犯同样的错误),我见过太多次这种情况,所以我觉得当这些bug悄悄进入你的代码时,所经历的痛苦是完全不值得的。
结论:
使用 map
和 filter
。它们可以防止一些难以诊断的作用域相关的bug。
附注:
如果适合你的情况,别忘了考虑使用 imap
和 ifilter
(在 itertools
中)!
情况说明
- 常见情况: 在python中,几乎总是建议使用列表推导式,因为这样能让初学者更容易理解你的代码。(这在其他语言中可能不适用,其他语言有其他的写法。)对于python程序员来说,列表推导式是迭代的标准写法,大家都习惯了。
- 不太常见的情况: 不过,如果你已经定义了一个函数,那么使用
map
也是合理的,尽管这被认为是不太“python风格”的。例如,map(sum, myLists)
比[sum(x) for x in myLists]
更简洁。这样你就不需要再定义一个临时变量(比如sum(x) for x...
或sum(_) for _...
或sum(readableName) for readableName...
),只为迭代而多写一次。filter
、reduce
以及itertools
模块中的其他函数也是如此:如果你已经有了一个函数,可以尝试一些函数式编程。这在某些情况下会提高可读性,但在其他情况下(比如初学者或多个参数)可能会降低可读性……不过,代码的可读性很大程度上还是取决于你的注释。 - 几乎不可能: 你可能会想在进行函数式编程时,把
map
当作一个纯抽象的函数来使用,比如映射map
,或者对map
进行柯里化,或者以其他方式讨论map
作为一个函数。在Haskell中,有一个叫fmap
的接口可以对任何数据结构进行映射。这在python中很少见,因为python的语法要求你使用生成器风格来进行迭代;你不能轻易地进行泛化。(这有时是好事,有时是坏事。)你可能能想到一些少见的python例子,在这些例子中map(f, *lists)
是合理的。我能想到的最接近的例子是sumEach = partial(map,sum)
,这是一行代码,大致等同于:
def sumEach(myLists):
return [sum(_) for _ in myLists]
- 直接使用
for
循环: 当然,你也可以直接使用for循环。虽然从函数式编程的角度看不够优雅,但在像python这样的命令式编程语言中,有时非局部变量能让代码更清晰,因为人们习惯于这样阅读代码。当你只是进行一些复杂操作而不是像列表推导式和map那样构建列表时,for循环通常是最有效的——至少在内存方面是有效的(不一定在时间上,最坏情况下我认为也只是一个常数因子,除非遇到一些罕见的垃圾回收问题)。
“Python风格”
我不喜欢“pythonic”这个词,因为在我看来,pythonic并不总是优雅的。不过,map
、filter
和类似的函数(比如非常有用的itertools
模块)在风格上可能被认为是不太“python风格”的。
懒惰
在效率方面,像大多数函数式编程构造一样,MAP可以是懒惰的,实际上在python中是懒惰的。这意味着你可以这样做(在python3中),你的电脑不会耗尽内存而丢失所有未保存的数据:
>>> map(str, range(10**100))
<map object at 0x2201d50>
试试用列表推导式来做这个:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
需要注意的是,列表推导式本身也是懒惰的,但python选择将其实现为非懒惰。不过,python确实支持以生成器表达式的形式实现懒惰的列表推导式,如下所示:
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
你可以基本上把[...]
语法看作是将生成器表达式传递给列表构造函数,比如list(x for x in range(5))
。
简短的例子
from operator import neg
print({x:x**2 for x in map(neg,range(5))})
print({x:x**2 for x in [-y for y in range(5)]})
print({x:x**2 for x in (-y for y in range(5))})
列表推导式是非懒惰的,因此可能需要更多内存(除非你使用生成器推导式)。方括号[...]
通常让事情变得明显,尤其是在一堆括号中。另一方面,有时你会变得冗长,比如输入[x for x in...
。只要你保持迭代变量简短,列表推导式通常更清晰,尤其是当你不缩进代码时。但你也可以选择缩进代码。
print(
{x:x**2 for x in (-y for y in range(5))}
)
或者把事情分开:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
python3的效率比较
map
现在是懒惰的:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^
因此,如果你不会使用所有数据,或者事先不知道需要多少数据,map
在python3中(以及python2或python3中的生成器表达式)会避免在最后一刻之前计算它们的值。通常,这种方式的好处会超过使用map
的开销。缺点是,这在python中相对于大多数函数式语言是非常有限的:你只有在按顺序从左到右访问数据时才能获得这个好处,因为python生成器表达式只能按x[0], x[1], x[2], ...
的顺序进行评估。
不过,假设我们有一个预先定义的函数f
,我们想要map
,并且我们通过立即强制评估list(...)
来忽略map
的懒惰性。我们会得到一些非常有趣的结果:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'
10000 loops, best of 3: 165/124/135 usec per loop ^^^^^^^^^^^^^^^
for list(<map object>)
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'
10000 loops, best of 3: 181/118/123 usec per loop ^^^^^^^^^^^^^^^^^^
for list(<generator>), probably optimized
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'
1000 loops, best of 3: 215/150/150 usec per loop ^^^^^^^^^^^^^^^^^^^^^^
for list(<generator>)
结果以AAA/BBB/CCC的形式呈现,其中A是在大约2010年的Intel工作站上使用python 3.?.?进行的,B和C是在大约2013年的AMD工作站上使用python 3.2.1进行的,硬件差异非常大。结果似乎表明,map和列表推导式在性能上是可比的,最强烈地受到其他随机因素的影响。我们唯一能确定的是,奇怪的是,尽管我们预期列表推导式[...]
的性能会优于生成器表达式(...)
,但map
的效率也高于生成器表达式(再次假设所有值都被评估/使用)。
重要的是要意识到,这些测试假设了一个非常简单的函数(恒等函数);不过这没关系,因为如果函数复杂,那么性能开销在程序的其他因素面前就微不足道了。(用其他简单的东西测试,比如f=lambda x:x+x
,可能仍然很有趣)
如果你擅长阅读python汇编代码,可以使用dis
模块查看背后实际发生了什么:
>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>)
3 MAKE_FUNCTION 0
6 LOAD_NAME 0 (xs)
9 GET_ITER
10 CALL_FUNCTION 1
13 RETURN_VALUE
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 18 (to 27)
9 STORE_FAST 1 (x)
12 LOAD_GLOBAL 0 (f)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1
21 LIST_APPEND 2
24 JUMP_ABSOLUTE 6
>> 27 RETURN_VALUE
>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
1 0 LOAD_NAME 0 (list)
3 LOAD_CONST 0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>)
6 MAKE_FUNCTION 0
9 LOAD_NAME 1 (xs)
12 GET_ITER
13 CALL_FUNCTION 1
16 CALL_FUNCTION 1
19 RETURN_VALUE
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (x)
9 LOAD_GLOBAL 0 (f)
12 LOAD_FAST 1 (x)
15 CALL_FUNCTION 1
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
1 0 LOAD_NAME 0 (list)
3 LOAD_NAME 1 (map)
6 LOAD_NAME 2 (f)
9 LOAD_NAME 3 (xs)
12 CALL_FUNCTION 2
15 CALL_FUNCTION 1
18 RETURN_VALUE
看起来使用[...]
语法比使用list(...)
更好。可惜map
类在反汇编时有点不透明,但我们可以通过速度测试来得出结论。
在某些情况下,使用 map 可能会稍微快一点(尤其是当你不是为了这个目的而写一个 lambda 函数,而是直接在 map 和 列表推导式 中使用同一个函数时)。而在其他情况下,列表推导式可能会更快,而且大多数(不是全部)Python 爱好者认为它们更直接、更清晰。
这是一个使用完全相同函数时 map 的微小速度优势的例子:
$ python -m timeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -m timeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop
这是一个当 map 需要一个 lambda 函数时,性能比较完全反转的例子:
$ python -m timeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -m timeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop