列表理解vs地图

2020-12-02 20:40:08 发布

您现在位置:Python中文网/ 问答频道 /正文

有没有理由更喜欢使用map()而不是列表理解,或者反之亦然?它们中的任何一个通常比另一个更有效还是被认为更像Python?

3条回答
网友
1楼 ·

map在某些情况下(当您不是为了这个目的而制作lambda,而是在map和listcomp中使用相同的函数时)可能在显微镜下更快。在其他情况下,列表理解可能更快,大多数(并非所有)Python认为它们更直接、更清晰。

使用完全相同的功能时,map的微小速度优势示例:

$ 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

当map需要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
网友
2楼 ·

案例

  • 常见情况:几乎总是,您需要在python中使用列表理解,因为您对阅读代码的新手程序员所做的事情将更加明显。(这不适用于其他语言,在这些语言中可能会用到其他习语。)您对python程序员所做的事情将更加明显,因为列表理解是python中用于迭代的事实标准;它们是预期的
  • 不太常见的情况:但是,如果您已经定义了一个函数,那么使用map通常是合理的,尽管它被认为是“不合理的”。例如,map(sum, myLists)[sum(x) for x in myLists]更优雅/简洁。您获得了不必构造虚拟变量(例如sum(x) for x...sum(_) for _...sum(readableName) for readableName...)的优雅,您只需反复输入两次即可。对于filterreduce以及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)中,非局部变量会使代码更加清晰,因为人们非常习惯这样阅读代码。For循环通常也是最有效的,当你只是做任何复杂的操作,而不是建立一个类似列表的理解和地图优化(例如,求和,或建立一棵树,等等),至少在内存方面是有效的(不一定是时间方面,我最坏的期望是一个常数因子,除非有一些罕见的病理性垃圾收集打嗝)。

“Python病”

我不喜欢“pythonic”这个词,因为我觉得pythonic在我眼里并不总是优雅的。然而,mapfilter以及类似的函数(比如非常有用的itertools模块)在风格上可能被认为是不通顺的。

懒惰

就效率而言,与大多数函数式编程结构一样,MAP可以是LAZY,实际上在python中是LAZY。这意味着您可以这样做(在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))})

列表理解是非惰性的,因此可能需要更多的内存(除非您使用generator comp恢复)。方括号[...]经常使事情变得明显,尤其是在一堆括号中。另一方面,有时你会像输入[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中的map(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>)

结果以A A A/BBB/CCC的形式显示,其中A是在使用python 3的circa-2010英特尔工作站上执行的。?。?,B和C是在一个使用python 3.2.1的circa-2013 AMD工作站上执行的,硬件非常不同。结果表明,映射和列表理解在性能上具有可比性,且受其他随机因素的影响最大。奇怪的是,虽然我们希望列表理解[...]比生成器表达式(...)执行得更好,但是map也比生成器表达式更有效(再次假设所有值都是求值/使用的)。

重要的是要认识到这些测试假设了一个非常简单的函数(identity函数);但是这很好,因为如果函数很复杂,那么与程序中的其他因素相比,性能开销可以忽略不计。(用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类对于反汇编来说有点不透明,但是我们可以通过速度测试来完成。

网友
3楼 ·

Python 2:应该使用mapfilter而不是列表理解

一个目标即使他们不是“Python”你也应该喜欢他们的原因是:
它们需要函数/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不在同一范围内。
直到我将内部块移动到代码的不同部分之后,才出现了问题(读:维护期间的问题,而不是开发期间的问题),我没有预料到。

是的,如果你从未犯过这个错误,那么列表理解会更优雅。
但从个人经验(以及看到其他人犯同样的错误)来看,我已经看到它发生了足够多的次数,我认为当这些错误潜入到代码中时,不值得你付出痛苦。

结论:

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

旁注:

如果适合你的情况,不要忘记考虑使用imapifilter(initertools)!

相关问题