-
12-09-2019 - |
题
有理由更喜欢使用 map()
过度列表理解还是反之亦然?它们中的任何一个通常比另一个更高效还是被认为更Python化?
解决方案
map
可以是微观上更快在某些情况下(当你不使为宗旨的λ,但是使用在地图相同的功能和一个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...
)您必须输入两次才能迭代。同样的论点也适用于filter
和reduce
以及任何来自itertools
模块:如果您已经有了一个方便的函数,您可以继续进行一些函数式编程。这在某些情况下获得了可读性,但在其他情况下却失去了可读性(例如新手程序员,多个参数)...但无论如何,代码的可读性很大程度上取决于您的注释。 - 几乎从不: :您可能想使用
map
在进行函数式编程时将函数作为纯抽象函数,在其中进行映射map
, ,或柯里化map
, ,或以其他方式受益于谈论map
作为一个函数。例如,在 Haskell 中,一个函子接口称为fmap
概括了任何数据结构上的映射。这在Python中非常罕见,因为Python语法迫使你使用生成器风格来谈论迭代;你不能轻易地概括它。(这有时是好的,有时是坏的。)你可能会想出一些罕见的 python 例子,其中map(f, *lists)
是一件合理的事情。我能想到的最接近的例子是sumEach = partial(map,sum)
, ,这是一个单行代码,大致相当于:
def sumEach(myLists):
return [sum(_) for _ in myLists]
- 只需使用一个
for
-环形: :当然,您也可以只使用 for 循环。虽然从函数式编程的角度来看并不那么优雅,但有时非局部变量会使命令式编程语言(例如 python)中的代码更清晰,因为人们非常习惯以这种方式阅读代码。一般来说,当您只是执行任何复杂的操作而不是像列表推导式和映射那样构建列表时,For 循环也是最有效的(例如,求和,或制作一棵树等)——至少在内存方面有效(不一定在时间方面,我预计最坏的情况是一个常数因子,除非出现一些罕见的病态垃圾收集问题)。
“Python主义”
我不喜欢“pythonic”这个词,因为我没有发现pythonic在我眼里总是优雅的。尽管如此, map
和 filter
和类似的功能(比如非常有用的 itertools
module) 就风格而言可能被认为是非Pythonic的。
懒惰
就效率而言,与大多数函数式编程结构一样, 地图可以是懒惰的, ,实际上在 python 中是懒惰的。这意味着你可以这样做(在 蟒蛇3)并且您的计算机不会耗尽内存并丢失所有未保存的数据:
>>> 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 ^^^^^^^^^
因此,如果您不会使用所有数据,或者提前不知道需要多少数据, map
在 python3 中(以及 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 年的 Intel 工作站上使用 python 3.??.? 执行的,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
类对于反汇编来说有点不透明,但我们可以通过速度测试来进行。
的Python 2:你应该使用map
和filter
而不是列表解析
。
这是目标原因,你应该更喜欢他们,即使他们不是“Python化”是这样的:点击 它们需要的功能/ 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
s并不在同一范围内点击。
这只是在我的移动内部块到这个问题上来代码的不同部分。(阅读:维修过程中的问题,不发展),而且我没想到它
是,如果你从来没有犯这样的错误的那么列表内涵是更优雅。结果 但是从个人的经验(以及看到别人再犯同样的错误),我已经看到了这一点足够的时间,我认为这是不值得的痛苦,你有当这些错误蔓延到你的代码要经过。
结论:
使用map
和filter
。他们阻止微妙难以诊断范围有关的错误。
边注:
不要忘了考虑使用imap
和ifilter
(在itertools
),如果他们是适合您的情况!
实际上, map
和列表推导式在 Python 3 语言中的行为完全不同。看一下下面的 Python 3 程序:
def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))
您可能期望它打印“[1, 4, 9]”行两次,但实际上它打印“[1, 4, 9]”,后跟“[]”。当你第一次看的时候 squares
它似乎表现为三个元素的序列,但第二次表现为空元素。
在 Python 2 语言中 map
返回一个简单的旧列表,就像两种语言中的列表推导式一样。关键是返回值 map
在 Python 3 中(和 imap
在Python 2) 中不是一个列表——它是一个迭代器!
与迭代列表不同,当您迭代迭代器时,元素会被消耗。这就是为什么 squares
最后看起来很空 print(list(squares))
线。
总结一下:
- 在处理迭代器时,您必须记住它们是有状态的,并且当您遍历它们时它们会发生变化。
- 列表更容易预测,因为它们仅在您显式改变它们时才会改变;他们是 容器.
- 还有一个奖励:数字、字符串和元组更加可预测,因为它们根本无法改变;他们是 价值观.
我觉得列表理解一般都更富有表现力的我想要比map
做 - 他们都完成它,但前者节省了试图理解这可能是一个复杂的lambda
表达的精神负担。
还有一个采访就在某个地方(我找不到它副手),其中圭多列出lambda
s和功能性功能的东西,他最后悔大约受理成Python,所以你可以让他们很非参数凭借该Python化。
下面是一种可能的情况下:
map(lambda op1,op2: op1*op2, list1, list2)
与
[op1*op2 for op1,op2 in zip(list1,list2)]
我猜的zip()是你需要的,如果你坚持以列表解析的地图沉迷于一个不幸的和不必要的开销。将是巨大的,如果有人澄清这是否肯定或否定。
如果你打算写任何异步,并行或分布式的代码,你可能会更喜欢列表理解map
- 因为大多数异步,并行,或分发包提供map
函数重载python的map
。然后通过将合适的函数map
到您的代码的其余部分,则可能不必修改您的原始的串行代码将它并行地运行(等)。
如此以来Python 3中, map()
是一个迭代,则需要记住你需要什么:一个迭代器或list
对象
由于@AlexMartelli已经提到,map()
比列表更快地理解只有当你不使用lambda
功能。
我将提供你一些时间比较。
<子>
Python的3.5.2和CPython的结果我已经使用木星笔记本和特别的 %timeit
的内置命令魔
结果的测量强>:■== 1000毫秒1000 == * 1000微秒= 1000 * 1000 * 1000纳秒子>
设定:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
内置功能:
%timeit map(sum, x_list) # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop
%timeit list(map(sum, x_list)) # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop
%timeit [sum(x) for x in x_list] # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop
lambda
功能:
%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop
%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop
%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop
有也可作为发电机表达这样的事情,请参阅 PEP- 0289 。所以我想将其添加到比较将是有用的
%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop
%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop
%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop
%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop
需要list
对象:
使用列表理解,如果它的自定义功能,使用list(map())
如果有内置函数
您无需list
对象,你只需要一个可迭代:
始终使用map()
!
我认为最Python的方式是使用列表理解,而不是map
和filter
。其原因是,列表解析比map
和filter
清晰。
In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension
In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter
In [3]: odd_cubes == odd_cubes_alt
Out[3]: True
正如你看到的,一个修真不需要额外lambda
表达式作为map
需求。此外,理解还允许过滤容易,而map
要求filter
以允许过滤。
我跑了快速测试,比较了三种方法用于调用对象的方法。的时间差,在这种情况下,是可以忽略的,是有问题的功能的问题(见@Alex马尔泰利的响应) 。在这里,我看着下面的方法:
# map_lambda
list(map(lambda x: x.add(), vals))
# map_operator
from operator import methodcaller
list(map(methodcaller("add"), vals))
# map_comprehension
[x.add() for x in vals]
我看着清单用于增加列表大小两个整数(Python的vals
)和浮点数(Python的int
)的(存储在变量float
)。以下伪类DummyNum
被认为是:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
具体而言,add
方法。所述__slots__
属性是在Python一个简单的优化,以限定由类(属性)所需的总存储器,减少存储器的大小。
以下是所得的曲线图。
如前所述,所使用的技术使得最小差,你应该在的方式,是最可读你,或者在特定情况下进行编码。在这种情况下,列表解析(map_comprehension
技术)是最快的对两种类型的加法中的对象,尤其是较短的列表。