列表理解与地图

2022-09-05 01:17:43

是否有理由更喜欢使用列表理解,反之亦然?它们中的任何一个通常比另一个更有效率还是被认为通常更pythonic?map()


答案 1

map在某些情况下,显微镜下可能会更快(当您不是为此目的制作lambda,而是在map和listcomp中使用相同的函数时)。在其他情况下,列表推导可能更快,大多数(不是全部)pythonistas认为它们更直接,更清晰。

使用完全相同的函数时 map 的微小速度优势示例:

$ python -m timeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -m timeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop

当映射需要 lambda 时,性能比较如何完全反转的示例:

$ python -m timeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -m timeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop

答案 2

  • 常见情况:几乎总是,你会想要在python中使用列表理解,因为对于阅读代码的新手程序员来说,你正在做的事情会更加明显。(这不适用于其他语言,其他习语可能适用。你对python程序员所做的甚至会更加明显,因为列表推导是python中迭代的事实标准;他们是意料之中的
  • 不太常见的情况:但是,如果您已经定义了一个函数,则通常使用它是合理的,尽管它被认为是“非pythonic”。例如,比 更优雅/简洁。您可以获得不必组成虚拟变量的优雅(例如 或 或 ), 您必须键入两次,只是为了迭代。对于 和 模块中的任何内容,同样的论点也成立:如果您已经有一个方便的函数,则可以继续进行一些函数式编程。这在某些情况下获得了可读性,而在其他情况下(例如,新手程序员,多个参数)则失去了可读性。但是你的代码的可读性在很大程度上取决于你的注释。mapmap(sum, myLists)[sum(x) for x in myLists]sum(x) for x...sum(_) for _...sum(readableName) for readableName...filterreduceitertools
  • 几乎从不:在进行函数式编程时,您可能希望将函数用作纯抽象函数,在那里您正在映射,或讨价还价,或者从作为函数进行讨论中受益。例如,在Haskell中,一个名为“泛化映射”的函子接口可以推广到任何数据结构上。这在python中非常罕见,因为python语法迫使你使用生成器样式来谈论迭代;你不能轻易地概括它。(这有时是好的,有时是坏的。你可能会想出一些罕见的python例子,这是一件合理的事情。我能想到的最接近的例子是 ,这是一个单行,非常粗略地等价于:mapmapmapmapfmapmap(f, *lists)sumEach = partial(map,sum)

def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • 只需使用 for 循环:当然,您也可以只使用 for 循环。虽然从函数式编程的角度来看并不那么优雅,但有时非局部变量会使命令式编程语言(如python)中的代码更清晰,因为人们非常习惯于以这种方式阅读代码。一般来说,当你只是做任何复杂的操作时,For循环也是最有效的,这些操作没有构建列表,比如列表推导和map被优化(例如求和,或做一棵树等) - 至少在内存方面是有效的(不一定是在时间方面,我最坏的情况是期望一个常数因子, 除了一些罕见的病理性垃圾收集打嗝)。

“Pythonism”

我不喜欢“pythonic”这个词,因为我发现pythonic在我眼中并不总是优雅的。然而,和类似的函数(如非常有用的模块)在风格方面可能被认为是非Pythonic的。mapfilteritertools

懒惰

在效率方面,像大多数函数式编程结构一样,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中的map(以及python2或python3中的生成器表达式)将避免计算它们的值,直到最后一刻需要。通常,这通常会超过使用地图的任何开销。缺点是,与大多数函数式语言相比,这在python中非常有限:只有当您“按顺序”从左到右访问数据时,您才能获得此好处,因为python生成器表达式只能以x[0],x[1],x[2],...的顺序进行计算。

但是,假设我们有一个我们想要的预制函数,并且我们忽略了通过立即强制评估的懒惰。我们得到了一些非常有趣的结果:fmapmaplist(...)

% 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.?.?,的Intel工作站上执行的,B和C是在大约2013年AMD工作站上使用python 3.2.1执行的,具有极其不同的硬件。结果似乎是地图和列表推导在性能上具有可比性,这受其他随机因素的影响最大。我们唯一能说的似乎是,奇怪的是,虽然我们希望列表推导比生成器表达式表现得更好,但也比生成器表达式更有效(再次假设所有值都被计算/使用)。[...](...)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