Python List Comprehension Vs. 地图
有理由更喜欢使用map()
不是列表理解,反之亦然? 他们中的任何一个通常更有效率或被认为通常比另一个更pythonic?
在某些情况下, map
可能会在显微镜下更快(当您不为此目的制作lambda时,但在地图和listcomp中使用相同的功能时)。 在其他情况下,列表理解可能会更快,大多数(并非全部)pythonistas认为它们更直接,更清晰。
当使用完全相同的功能时,地图的速度优势很小的一个例子:
$ python -mtimeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -mtimeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop
当地图需要lambda时,性能比较如何完全颠倒的示例:
$ python -mtimeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -mtimeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
案例
map
通常是合理的,尽管它被认为是“unpythonic”。 例如, map(sum, myLists)
比[sum(x) for x in myLists]
更优雅/简洁。 您可以获得无需组成虚拟变量的优雅(例如sum(x) for x...
或sum(_) for _...
或sum(readableName) for readableName...
),您必须键入两次,只是为了迭代。 filter
和reduce
同样适用于itertools
模块:如果你已经有了方便的功能,你可以继续做一些函数式编程。 这在某些情况下会增加可读性,并在其他情况下会丢失(例如新手程序员,多个参数)......但是代码的可读性高度取决于您的意见。 map
函数作为纯粹的抽象函数,在那里你映射map
或者currying map
,或者通过谈论map
作为函数来获益。 例如,在Haskell中,称为fmap
的函子接口概括了映射到任何数据结构。 这在python中非常罕见,因为python语法迫使你使用generator-style来谈论迭代; 你不能简单地概括它。 (这有时很好,有时候很糟糕。)你可能会想出罕见的python示例, map(f, *lists)
是一个合理的事情。 我能想出的最接近的例子是sumEach = partial(map,sum)
,这是一个非常大致等价于下面的单行数据: def sumEach(myLists):
return [sum(_) for _ in myLists]
“Pythonism”
我不喜欢“pythonic”这个词,因为我不觉得Pythonic在我眼中总是优雅的。 尽管如此, map
和filter
和类似的函数(比如非常有用的itertools
模块)在风格上可能被认为是unpythonic。
怠惰
就效率而言,与大多数函数式编程结构一样, 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 ^^^^^^^^^
因此,如果您不会使用所有数据,或者事先不知道需要多少数据,则可以使用python3(以及python2或python3中的生成器表达式)进行map
以避免在必要的最后时刻计算它们的值。 通常这通常会超过使用map
任何开销。 缺点是python与大多数函数式语言相比是非常有限的:如果您按顺序从左到右地访问数据,您只会得到这种好处,因为python生成器表达式只能评估x[0], x[1], x[2], ...
但是让我们说,我们有一个预制的功能f
我们希望map
,和我们忽略的懒惰map
通过立即强制评估与list(...)
我们得到一些非常有趣的结果:
% 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年的英特尔工作站上使用python 3执行,而B和C则使用大约2013年的AMD工作站,使用python 3.2.1,硬件极其不同。 结果似乎是地图和列表理解在性能上是可比较的,这是受其他随机因素影响最大的。 我们可以告诉的唯一的事情似乎是,奇怪的是,虽然我们期待list解析[...]
执行除发电机表情好(...)
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
和filter
而不是列表解析。
即使他们不是“Pythonic”,你应该更喜欢他们的客观原因是:
它们需要函数/ lambdas作为参数,这引入了一个新的范围 。
我被这个不止一次咬过了:
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
不在同一个范围内。
只是在我将内部块移到代码的另一部分,以至于问题出现之后(阅读:维护期间的问题,而不是开发),并且我没有想到它。
是的,如果你从不犯这个错误,那么列表解析更优雅。
但是,从个人经验(以及看到其他人犯同样的错误)中,我发现它发生的次数足够多,我认为当这些错误蔓延到您的代码中时,您不得不经历的痛苦。
结论:
使用map
和filter
。 它们可以防止细微的难以诊断的范围相关的错误。
边注:
不要忘记考虑使用imap
和ifilter
(在itertools
),如果它们适合您的情况!