是否有理由更喜欢使用map()
列表理解,反之亦然?它们中的任何一个通常比另一个更有效还是被认为通常更pythonic?
13 回答
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
当 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
案例
- 常见情况:几乎总是,您会希望在python中使用列表推导,因为对于阅读您的代码的新手程序员来说,您正在做的事情会更加明显。(这不适用于其他语言,其他成语可能适用。)你对 python 程序员所做的事情会更加明显,因为列表推导是 python 中用于迭代的事实上的标准;他们是预料之中的。
- 不太常见的情况:但是,如果您已经定义了一个函数,那么使用它通常是合理的
map
,尽管它被认为是“unpythonic”。例如,map(sum, myLists)
比 . 更优雅/简洁[sum(x) for x in myLists]
。您获得了不必编写必须键入两次的虚拟变量(例如sum(x) for x...
orsum(_) for _...
或)的优雅,只是为了迭代。sum(readableName) for readableName...
相同的论点适用于模块中的filter
andreduce
和任何东西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
-loop:您当然也可以仅使用 for 循环。虽然从函数式编程的角度来看并不那么优雅,但有时非局部变量会使命令式编程语言(如 python)中的代码更清晰,因为人们非常习惯以这种方式阅读代码。通常,当您仅执行任何复杂操作时,for 循环也是最有效的在记忆方面有效(不一定在时间方面,我希望在最坏的情况下是一个恒定因素,除非出现一些罕见的病态垃圾收集打嗝)。
“蟒蛇主义”
我不喜欢“pythonic”这个词,因为我发现 pythonic 在我眼中并不总是优雅的。然而,map
andfilter
和类似的函数(比如非常有用的itertools
模块)在风格上可能被认为是不符合 Python 的。
懒惰
就效率而言,与大多数函数式编程结构一样,MAP CAN BE 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...
。只要您保持迭代器变量简短,如果您不缩进代码,列表推导通常会更清晰。但是你总是可以缩进你的代码。
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 生成器表达式只能按 order 求值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
andfilter
而不是列表推导。
即使它们不是“Pythonic”,你也应该更喜欢它们的一个客观原因是:
它们需要函数/lambdas 作为参数,这引入了一个新的范围。
我不止一次被这个咬过:
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
and ifilter
(in )!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 op1,op2: op1*op2, list1, list2)
相对:
[op1*op2 for op1,op2 in zip(list1,list2)]
如果您坚持使用列表推导而不是地图,我猜 zip() 是您需要沉迷的不幸且不必要的开销。如果有人肯定或否定地澄清这一点,那就太好了。
如果您打算编写任何异步、并行或分布式代码,您可能更喜欢map
列表推导式——因为大多数异步、并行或分布式包都提供了一个map
函数来重载 python 的map
. 然后通过将适当的map
函数传递给您的代码的其余部分,您可能不必修改原始串行代码以使其并行运行(等)。
我发现列表推导通常比我想要做的更能表达map
- 他们都完成了它,但前者节省了试图理解可能是复杂lambda
表达式的精神负担。
还有一个采访在某处(我不能随便找到),Guido 将lambda
s 和函数函数列为他最后悔接受 Python 的东西,所以你可以说它们是非 Pythonic 的那个。
因此,既然 Python 3map()
是一个迭代器,你需要记住你需要什么:一个迭代器或list
对象。
正如@AlexMartelli 已经提到的那样,map()
仅当您不使用lambda
函数时才比列表理解更快。
我将向您展示一些时间比较。
Python 3.5.2 和 CPython
我使用了Jupiter notebook,尤其是%timeit
内置的魔法命令
测量:s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns
设置:
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()
!
我进行了一个快速测试,比较了调用对象方法的三种方法。在这种情况下,时间差可以忽略不计,并且与所讨论的功能有关(请参阅@Alex Martelli 的回复)。在这里,我查看了以下方法:
# 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]
vals
我查看了整数(Python int
)和浮点数(Python)的列表(存储在变量中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
技术)对于对象中的两种类型的添加是最快的,尤其是对于较短的列表。
访问此 pastebin以获取用于生成绘图和数据的源。
我用perfplot(我的一个项目)对一些结果进行了计时。
正如其他人所指出的,map
实际上只返回一个迭代器,所以它是一个恒定时间的操作。当通过 实现迭代器时list()
,它与列表推导相当。根据表达式的不同,任何一个都可能有轻微的优势,但几乎不重要。
请注意,像这样的算术运算在 NumPyx ** 2
中要快得多,尤其是在输入数据已经是 NumPy 数组的情况下。
hex
:
x ** 2
:
重现绘图的代码:
import perfplot
def standalone_map(data):
return map(hex, data)
def list_map(data):
return list(map(hex, data))
def comprehension(data):
return [hex(x) for x in data]
b = perfplot.bench(
setup=lambda n: list(range(n)),
kernels=[standalone_map, list_map, comprehension],
n_range=[2 ** k for k in range(20)],
equality_check=None,
)
b.save("out.png")
b.show()
import perfplot
import numpy as np
def standalone_map(data):
return map(lambda x: x ** 2, data[0])
def list_map(data):
return list(map(lambda x: x ** 2, data[0]))
def comprehension(data):
return [x ** 2 for x in data[0]]
def numpy_asarray(data):
return np.asarray(data[0]) ** 2
def numpy_direct(data):
return data[1] ** 2
b = perfplot.bench(
setup=lambda n: (list(range(n)), np.arange(n)),
kernels=[standalone_map, list_map, comprehension, numpy_direct, numpy_asarray],
n_range=[2 ** k for k in range(20)],
equality_check=None,
)
b.save("out2.png")
b.show()
我认为最 Pythonic 的方法是使用列表推导而不是map
and 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-martelli 的代码,但发现了一些差异
python -mtimeit -s "xs=range(123456)" "map(hex, xs)"
1000000 loops, best of 5: 218 nsec per loop
python -mtimeit -s "xs=range(123456)" "[hex(x) for x in xs]"
10 loops, best of 5: 19.4 msec per loop
map 即使对于非常大的范围也需要相同的时间,而使用列表理解需要很多时间,这从我的代码中可以看出。所以除了被认为是“unpythonic”之外,我还没有遇到任何与地图使用有关的性能问题。