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... ),您必须键入两次,只是为了迭代。 filterreduce同样适用于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在我眼中总是优雅的。 尽pipe如此, mapfilter和类似的函数(比如非常有用的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来完成。

你应该使用mapfilter而不是列表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就更加优雅。
但是,从个人经验(并看到其他人犯同样的错误),我已经看到了足够多的时间,我认为当这些错误蔓延到你的代码中时,你不得不经历的痛苦。

结论:

使用mapfilter 。 它们可以防止细微的难以诊断的范围相关的错误。

边注:

不要忘了考虑使用imapifilter (在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 – 他们都完成了,但是前者保存了试图理解可能是一个复杂的lambdaexpression式的精神负担。

在那里还有一个面试的地方(我无法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无法编译这些函数。

http://psyco.sourceforge.net/

所以,既然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的方式是使用列表理解,而不是mapfilter 。 原因是列表推导比mapfilter更清晰。

 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 

正如你所看到的,理解不需要额外的lambdaexpression式作为map需求。 此外,理解也允许过滤容易,而map需要filter以允许过滤。