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
案例
map
通常是合理的,尽管它被认为是 “非 pythonic” 的。例如, map(sum, myLists)
[sum(x) for x in myLists]
更为优雅 / 简洁。您可以不必键入一个虚拟变量(例如sum(x) for x...
或sum(_) for _...
或sum(readableName) for readableName...
),这非常优雅。两次,只是要迭代。相同的参数适用于filter
和reduce
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 在我眼中并不总是那么优雅。不过, map
和filter
以及类似的功能(例如非常有用的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
类对于反汇编而言有点不透明,但是我们可以通过速度测试来确定。
map
和filter
而不是列表推导。即使它们不是 “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
不在同一范围内。
只是在我将内部块移到代码的不同部分之后,问题才出现(阅读:维护期间的问题,而不是开发过程中的问题),而且我没有想到这一点。
是的,如果您从未犯过此错误,则列表理解会更优雅。
但是从个人经验(以及看到其他人犯同样的错误)的角度来看,我已经看到它发生了足够多的时间,以至于当这些错误潜入您的代码中时,您不应该经历这种痛苦。
使用map
和filter
。它们可以防止与范围相关的细微难以诊断的错误。
如果适合您的情况,请不要忘记考虑使用imap
和ifilter
(在itertools