列表理解与映射

新手上路,请多包涵

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

原文由 TimothyAWiseman 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 252
2 个回答

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

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

当 map 需要 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

原文由 Alex Martelli 发布,翻译遵循 CC BY-SA 4.0 许可协议

案例

  • 常见情况:几乎总是,您会希望在 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 或柯里 map ,38-7或者以其他方式从谈论 map 作为一个函数中受益。例如,在 Haskell 中,一个名为 fmap 的仿函数接口泛化了对任何数据结构的映射。这在 python 中很少见,因为 python 语法强制你使用生成器风格来谈论迭代;你不能轻易概括它。 (这有时好有时坏。)您可能会想出罕见的 python 示例,其中 map(f, *lists) 是合理的做法。我能想出的最接近的例子是 sumEach = partial(map,sum) ,这是一个非常粗略地相当于:
 def sumEach(myLists):
    return [sum(_) for _ in myLists]

  • 仅使用 for -loop :您当然也可以仅使用 for 循环。虽然从函数式编程的角度来看并不那么优雅,但有时非局部变量会使命令式编程语言(如 python)中的代码更清晰,因为人们非常习惯以这种方式阅读代码。通常,当您仅执行任何不构建列表的复杂操作时,for 循环也是最有效的,例如列表推导和地图优化(例如求和或制作树等) - 至少在内存方面高效(不一定在时间方面,我希望在最坏的情况下是一个常数因素,除非出现一些罕见的病态垃圾收集打嗝)。

“蟒蛇主义”

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

懒惰

在效率方面,像大多数函数式编程构造一样, 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 中的生成器表达式)将避免计算它们的值,直到最后一次必要的时刻。通常这通常会超过使用 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 执行的,具有截然不同的硬件。结果似乎是 map 和 list comprehension 在性能上具有可比性,这受其他随机因素的影响最大。我们唯一可以说的似乎是,奇怪的是,虽然我们期望列表 [...] 比生成器表达式 (...)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 类对于反汇编来说有点不透明,但我们可以通过速度测试来弥补。

原文由 ninjagecko 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题