Python List Comprehension VS. 地图
有理由更喜欢使用map()
不是列表理解,反之亦然? 他们中的任何一个通常比另一个更有效率或被认为通常更为pythonic?
在某些情况下, map
可能在显微镜上更快(当你不是为了目的而做一个lambda,而是在map和listcomp中使用相同的函数的时候)。 在其他情况下,列表理解可能会更快,大多数(不是全部)pythonistas认为它们更直接,更清晰。
使用完全相同的function时,地图的微小速度优势的一个例子:
$ 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
案例
- 常见案例 :几乎总是会用到python中的列表理解,因为对于新手程序员来说,读取代码会更明显。 (这不适用于其他语言,其他成语可能适用。)它甚至会更明显,你正在做的Python程序员,因为列表parsing是事实上的python标准的迭代; 他们预计 。
- 不太常见的情况 :但是,如果你已经定义了一个函数 ,使用
map
也是合理的,虽然它被认为是“unpythonic”。 例如,map(sum, myLists)
比[sum(x) for x in myLists]
更优雅。 您可以获得不必组成虚拟variables的优雅(例如sum(x) for x...
或sum(_) for _...
或sum(readableName) for readableName...
),您必须键入两次,只是为了迭代。filter
和reduce
同样适用于itertools
模块:如果你已经有一个方便的函数,你可以继续做一些函数式的编程。 这在某些情况下可以获得可读性,而在其他情况下(例如新手程序员,多个参数)则会失去可读性,但是代码的可读性高度取决于您的意见。 - 几乎从来没有 :您可能希望在执行函数式编程时使用
map
函数作为纯粹的抽象函数,在这种函数式编程中,映射map
或curryingmap
,或者将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在我眼中总是优雅的。 尽pipe如此, 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 #
请注意,列表parsing本身也是懒惰的,但是python已经select将它们实现为非懒惰 。 尽pipe如此,python确实支持以生成器expression式forms的懒列表推导,如下所示:
>>> (str(n) for n in range(10**100)) <generator object <genexpr> at 0xacbdef>
你可以基本上把传入一个生成器expression式的语法看成是列表的构造函数,比如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))})
列表推导是非懒惰的,所以可能需要更多的内存(除非你使用生成器理解)。 方括号常常使事情变得明显,特别是在括号内乱七八糟的时候。 另一方面,有时你最终会像input[x for x in...
只要你保持你的迭代器variables的简短,列表parsing通常更清晰,如果你不缩进你的代码。 但是你总是可以缩进你的代码。
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 ^^^^^^^^^
因此,如果您不会使用所有的数据,或者事先不知道需要多less数据,那么map
到python3(以及python2或python3中的生成器expression式)将避免直到最后一刻才计算它们的值。 通常这通常会大于使用map
任何开销。 不足之处在于python与大多数函数式语言相比是非常有限的:如果您按顺序从左到右地访问数据,则只能得到这个好处,因为python生成器expression式只能被评估为x[0], x[1], x[2], ...
然而让我们说,我们有一个预制的function,我们想要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的forms在一台大约在2010年的英特尔工作站上使用python 3执行A,B和C是在大约2013年的AMD工作站上用python 3.2.1执行的,硬件极其不同 结果似乎是地图和列表的理解在性能上是可比的,受其他随机因素影响最大。 我们唯一可以说的是,奇怪的是,虽然我们期望列表parsing比生成器expression式(...)
更好地执行,但是map
也是更有效的,即生成器expression式(再次假设所有的值都是评价/使用)。
认识到这些testing假设一个非常简单的函数(身份函数)是很重要的。 然而,这是好的,因为如果function复杂,那么与程序中的其他因素相比,性能开销可以忽略不计。 (用f=lambda x:x+x
等其他简单的东西来testing可能还是有趣的)
如果您熟练阅读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
类对于反汇编有点不透明,但是我们可以通过我们的速度testing来完成。
你应该使用map
和filter
而不是列表parsing。
即使他们不是“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)
那么一切都会好的。
你可以说我在相同的范围内使用相同的variables名是愚蠢的。
我没有。 代码原本是好的 – 两个x
不在同一个范围内。
只是在将内部块移到代码的不同部分(问题出现在维护期间的问题而不是开发)之后,我才意识到这一点。
是的, 如果你从不犯这个错误,那么列表parsing就更加优雅。
但是,从个人经验(并看到其他人犯同样的错误),我已经看到了足够多的时间,我认为当这些错误蔓延到你的代码中时,你不得不经历的痛苦。
结论:
使用map
和filter
。 它们可以防止细微的难以诊断的范围相关的错误。
边注:
不要忘了考虑使用imap
和ifilter
(在itertools
),如果他们适合您的情况!
实际上,在Python 3语言中, map
和列表的理解行为完全不同。 看看下面的Python 3程序:
def square(x): return x*x squares = map(square, [1, 2, 3]) print(list(squares)) print(list(squares))
您可能会期望它打印两行“[1,4,9]”,而是打印“[1,4,9]”,然后打印“[]”。 第一次看squares
它似乎performance为三个元素的序列,但第二次是空的。
在Python 2中,语言map
返回一个普通的旧列表,就像列表parsing在两种语言中一样。 关键是Python 3中的map
的返回值(和Python 2中的imap
)不是一个列表 – 它是一个迭代器!
当你遍历一个迭代器,而不是迭代一个列表时,这些元素被消耗掉了。 这就是为什么在最后一个print(list(squares))
行中看起来是空的。
总结:
- 在处理迭代器时,你必须记住它们是有状态的,当你遍历它们时,它们会发生变异。
- 列表是更可预测的,因为它们只有在你明确地改变它们时才会改变。 他们是容器 。
- 数字,string和元组更加可预测,因为它们根本不能改变; 他们是价值观 。
我发现列表理解通常比我们想要做的更能expression – 他们都完成了,但是前者保存了试图理解可能是一个复杂的lambda
expression式的精神负担。
在那里还有一个面试的地方(我无法find它)Guido列出了lambda
和function函数,他最感到遗憾的是接受Python,所以你可以说他们是非Pythonic的凭借这一点。
这是一个可能的情况:
map(lambda op1,op2: op1*op2, list1, list2)
与:
[op1*op2 for op1,op2 in zip(list1,list2)]
我猜测zip()是一个不幸和不必要的开销,如果你坚持使用列表parsing而不是地图,你需要沉迷于这个开销。 如果有人澄清这是否肯定或否定,这将是伟大的。
如果您打算编写任何asynchronous,并行或分布式代码,那么您可能更愿意map
列表map
到列表parsing上 – 因为大多数asynchronous,并行或分布式软件包提供了一个map
函数来重载python的map
。 然后,通过将适当的map
函数传递给代码的其余部分,您可能不必修改原始的串行代码以使其并行运行(等等)。
使用map()和filter()的列表理解的另一个原因是Psyco无法编译这些函数。
所以,既然Python 3, map()
是一个迭代器,你需要记住你需要什么:一个迭代器或list
对象。
正如@AlexMartelli已经提到的 ,只有在不使用lambda
函数的情况下, map()
才比列表理解更快。
我会给你一些比较。
Python 3.5.2和CPython
我已经使用木星笔记本 ,特别是%timeit
内置的魔术指令
测量 :s == 1000 ms == 1000 * 1000μs= 1000 * 1000 * 1000 ns
build立:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)] i_list = list(range(1000))
内置function:
%timeit map(sum, x_list) # creating iterator object # Output: The slowest run took 9.91 times longer than the fastest. # This could mean that an intermediate result is being cached. # 1000000 loops, best of 3: 277 ns per loop %timeit list(map(sum, x_list)) # creating list with map # Output: 1000 loops, best of 3: 214 µs per loop %timeit [sum(x) for x in x_list] # creating list with list comprehension # Output: 1000 loops, best of 3: 290 µs per loop
lambda
函数:
%timeit map(lambda i: i+1, i_list) # Output: The slowest run took 8.64 times longer than the fastest. # This could mean that an intermediate result is being cached. # 1000000 loops, best of 3: 325 ns per loop %timeit list(map(lambda i: i+1, i_list)) # Output: 1000 loops, best of 3: 183 µs per loop %timeit [i+1 for i in i_list] # Output: 10000 loops, best of 3: 84.2 µs per loop
还有发电机expression式这样的东西,见PEP-0289 。 所以我认为将其添加到比较将是有用的
%timeit (sum(i) for i in x_list) # Output: The slowest run took 6.66 times longer than the fastest. # This could mean that an intermediate result is being cached. # 1000000 loops, best of 3: 495 ns per loop %timeit list((sum(x) for x in x_list)) # Output: 1000 loops, best of 3: 319 µs per loop %timeit (i+1 for i in i_list) # Output: The slowest run took 6.83 times longer than the fastest. # This could mean that an intermediate result is being cached. # 1000000 loops, best of 3: 506 ns per loop %timeit list((i+1 for i in i_list)) # Output: 10000 loops, best of 3: 125 µs per loop
你需要list
对象:
如果是自定义函数,则使用列表理解;如果有内build函数,则使用list(map())
你不需要list
对象,你只需要迭代一个:
总是使用map()
!
我认为最Pythonic的方式是使用列表理解,而不是map
和filter
。 原因是列表推导比map
和filter
更清晰。
In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter In [3]: odd_cubes == odd_cubes_alt Out[3]: True
正如你所看到的,理解不需要额外的lambda
expression式作为map
需求。 此外,理解也允许过滤容易,而map
需要filter
以允许过滤。