协慌网

登录 贡献 社区

列表理解与地图

有什么理由比列表理解map()它们中的一个通常比另一个效率更高,或者通常被认为比另一个更 Python 化吗?

答案

map在微观上可能会更快(当您不是出于此目的而使用 lambda 时,而是在 map 和 listcomp 中使用相同的功能)。在其他情况下,列表理解可能会更快,并且大多数(并非全部)pythonista 用户认为列表更直接,更清晰。

使用完全相同的函数时 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

当地图需要使用 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通常是合理的,尽管它被认为是 “非 pythonic” 的。例如, 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 语法迫使您使用生成器风格来谈论迭代。您不能轻易将其概括。 (这有时是好事,有时是坏事。)您可能会想出一些罕见的 python 示例,其中map(f, *lists)是合理的做法。我能想到的最接近的示例是sumEach = partial(map,sum) ,它是一种单行代码,大致等效于:

def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • 只使用for -loop :当然,您也可以只使用 for -loop。尽管从功能编程的角度来看并不那么优雅,但有时非局部变量使命令式编程语言(如 python)中的代码更清晰,因为人们已经非常习惯于以这种方式读取代码。通常,对于仅执行未构建列表的复杂操作(例如列表理解和映射)的 for 循环,其效率最高(例如,求和或制作树等)- 至少就内存而言,它是高效的(不必在时间上,我希望在最坏的情况下,它是一个恒定的因素,除非出现一些罕见的病理性垃圾收集问题)。

“Python 主义”

我不喜欢 “pythonic” 这个词,因为我发现 pythonic 在我眼中并不总是那么优雅。不过, mapfilter以及类似的功能(例如非常有用的itertools模块)可能被认为是不合逻辑的。

懒惰

就效率而言,就像大多数函数式编程构造一样, MAP 可以是 LAZY ,实际上在 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 选择将其实现为 non-lazy 。不过,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...键入 [x for x ...]。只要保持迭代器变量简短,如果不缩进代码,列表解析通常会更加清晰。但是您总是可以缩进您的代码。

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            ^^^^^^^^^

因此,如果您将不使用所有数据,或者不提前知道需要多少数据,则map (在 python2 或 python3 中生成器表达式)将避免在最后一刻之前计算它们的值。通常,这通常会超过使用map任何开销。缺点是,与大多数功能语言相反,这在 python 中非常有限:只有按 “顺序” 从左到右访问数据时,您才能获得此好处,因为 python 生成器表达式只能按x[0], x[1], x[2], ...

但是,假设我们有一个要map f ,并且我们通过立即强制使用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>)

结果为 AAA / BBB / CCC 格式,其中 A 在带有 python 3。?。?的大约 2010 年英特尔工作站上执行,而 B 和 C 则是在 python 3.2.1 的大约 2013 年 AMD 工作站上执行,具有截然不同的硬件结果似乎是,地图和列表的理解在性能上是可比的,这受其他随机因素的影响最大。我们可以告诉的唯一的事情似乎是,奇怪的是,虽然我们期待 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类对于反汇编而言有点不透明,但是我们可以通过速度测试来确定。

Python 2:您应该使用mapfilter而不是列表推导。

即使它们不是 “Pythonic” 的,您还是还是偏爱它们的一个客观原因是:
它们需要函数 / lambda 作为参数,从而引入了新的作用域

我被这个不止一次地咬了:

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