Python List Comprehension Vs. 地图


在某些情况下, 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

案例

  • 常见案例 :几乎总是,你会希望在Python中使用列表理解,因为它会更明显地让你的新手程序员读取你的代码。 (这不适用于其他语言,其他成语可能适用。)它甚至会更明显地表明你对Python程序员所做的事情,因为列表解析是Python中迭代的事实标准; 他们预计。
  • 不太常见的情况 :但是,如果您已经定义了一个函数,使用map通常是合理的,尽管它被认为是“unpythonic”。 例如, map(sum, myLists)[sum(x) for x in myLists]更优雅/简洁。 您可以获得无需组成虚拟变量的优雅(例如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]
    
  • 只需使用for -loop :你当然也可以使用for循环。 虽然从函数式编程的角度来看并不优雅,但有时​​候非局部变量使得编程语言(如python)中的代码变得更加清晰,因为人们非常习惯以这种方式阅读代码。 For循环通常也是最有效的,当你只是做任何复杂的操作,而不是建立一个像列表一样的列表时,解析和映射被优化(例如求和,或者生成树等) - 至少在记忆方面是有效的(不一定就时间而言,除非是一些罕见的病态垃圾收集打嗝,否则我最不希望的是一个恒定的因素)。
  • “Pythonism”

    我不喜欢“pythonic”这个词,因为我不觉得Pythonic在我眼中总是优雅的。 尽管如此, 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 #
    

    请注意,列表解析本质上也是懒惰的,但是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类对于反汇编有点不透明,但是我们可以通过我们的速度测试来完成。


    您应该使用mapfilter而不是列表解析。

    即使他们不是“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不在同一个范围内。
    只是在我内部块移到代码的另一部分,以至于问题出现之后(阅读:维护期间的问题,而不是开发),并且我没有想到它。

    是的,如果你从不犯这个错误,那么列表解析更优雅。
    但是,从个人经验(以及看到其他人犯同样的错误)中,我发现它发生的次数足够多,我认为当这些错误蔓延到您的代码中时,您不得不经历的痛苦。

    结论:

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

    边注:

    不要忘记考虑使用imapifilter (在itertools ),如果它们适合您的情况!

    链接地址: http://www.djcxy.com/p/53575.html

    上一篇: Python List Comprehension Vs. Map

    下一篇: How many bytes per element are there in a Python list (tuple)?