在使用Python编程语言中,使用 map() 函数和列表推导式有没有具体的优势?它们哪个更高效或被认为更符合 Python 的编程风格呢?
在使用Python编程语言中,使用 map() 函数和列表推导式有没有具体的优势?它们哪个更高效或被认为更符合 Python 的编程风格呢?
map在某些情况下可能微观上更快(当您不是为了目的而制作lambda,而是在map和list comprehension中使用相同的函数时)。列表推导式在其他情况下可能更快,并且大多数(但不是全部)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
map(operator.attrgetter('foo'), objs)
比 [o.foo for o in objs]
更易读?! - Alex Martelli案例
map
,尽管这被认为是“不符合Python风格”。例如,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
的functor接口通用地映射到任何数据结构。这在Python中非常罕见,因为Python语法迫使您使用生成器样式来讨论迭代; 你无法轻松泛化它。(有时候好,有时候坏。) 你可能会找到一些罕见的Python示例,其中map(f,* lists)
是可以做的合理的事情。我能想到的最接近的例子是sumEach = partial(map,sum)
,这是一个非常粗略等价的单行代码:def sumEach(myLists):
return [sum(_) for _ in myLists]
"Python风格"
我不喜欢“pythonic”这个词,因为我不认为Pythonic总是优雅的。尽管如此,像map
和filter
以及类似的函数(如非常有用的itertools
模块)在风格上可能被认为不够Pythonic。
懒惰求值
就效率而言,像大多数函数式编程结构一样,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 ^^^^^^^^^
因此,如果您不会使用所有数据,或者事先不知道需要多少数据,map
在Python3中(以及在Python2或Python3中的生成器表达式)将避免计算它们的值,直到必要的最后一刻。通常情况下,这通常比使用map
带来的任何开销更有利。缺点是,与大多数函数式语言相比,Python中非常有限:只有在按顺序访问数据时才能获得此优势,因为Python生成器表达式只能按顺序计算x [0],x [1],x [2],...
。f
想要map
,并且我们通过立即使用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是在约2010年的Intel工作站上使用Python 3.?.?执行的,而B和C是在约2013年的AMD工作站上使用Python 3.2.1执行的,具有非常不同的硬件。结果似乎是map和列表推导在性能上可比,最受其他随机因素的影响。我们唯一可以确定的是,奇怪的是,虽然我们期望列表推导 [...]
的性能优于生成器表达式 (...)
,但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
以及标准库中的itertools
被认为是本质上不好的风格是不公平的。除非GvR实际上说过它们要么是一个可怕的错误,要么仅仅是为了性能而存在,否则如果“Pythonicness”确实是这样说的,那么唯一自然的结论就是把它当作无聊的东西忘掉;-) - Steve Jessopmap
/filter
是一个好主意,只有其他Python程序员的反叛行动才让它们留在内置命名空间中(而reduce
则被移动到了functools
)。我个人不同意(如果使用预定义的特别是内置函数,则map
和filter
很好,只要不需要使用lambda
),但是GvR多年来一直称它们不符合 Pythonic。 - ShadowRangeritertools
?我引用这个答案的部分是让我感到困惑的主要声明。我不知道在他理想的世界中,map
和filter
是否会移动到itertools
(或functools
),或者完全消失,但无论哪种情况,一旦有人说itertools
在其整体上不符合Python风格,那么我真的不知道“Pythonic”应该意味着什么,但我认为它不可能与“GvR建议人们使用的东西”相似。 - Steve Jessopmap
/filter
,没有涉及到 itertools
。函数式编程在Python中是很符合规范的(itertools
、functools
和operator
都是专为函数式编程设计的,我经常在Python中使用函数式语法),而且 itertools
提供了一些很难自己实现的功能。问题在于 map
和 filter
与生成器表达式重复,这让 Guido 讨厌它们。itertools
一直以来都是很好的。 - ShadowRangerlist(map(foo, x))
更可取。自从Python 3以后,如果你需要一个列表而不是生成器,我认为在可读性方面,map
、filter
和类似的函数都不如列表推导式。至于生成器推导式的其余部分,则没有问题。 - Puffmap
和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
中)!
map
和/或filter
仍然不是一个逻辑上的理由。如果有的话,最直接和逻辑的翻译来避免你的问题不是map(lambda x: x ** 2, numbers)
而是生成器表达式 list(x ** 2 for x in numbers)
,正如JeromeJ已经指出的那样,它不会泄漏。Mehrdad,请不要太在意被踩,我只是非常不同意你的推理。 - wim实际上,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
时,它似乎是一个由三个元素组成的序列,但第二次却为空。map
返回一个普通的列表,就像两种语言中的列表推导式一样。关键在于,在Python 3中map
的返回值(在Python 2中为imap
)不是列表,而是迭代器!print(list(squares))
行中,squares
看起来是空的原因。map
生成一个数据结构,而不是迭代器。但是也许懒惰的迭代器比懒惰的数据结构更容易。值得思考。谢谢@MnZrK - semiomant这里是一个可能的情况:
map(lambda op1,op2: op1*op2, list1, list2)
对比:
[op1*op2 for op1,op2 in zip(list1,list2)]
我猜测使用zip()是一个不幸而且不必要的开销,如果你坚持使用列表推导式而不是map()。希望有人能明确肯定或否定这一点。
itertools.izip
使zip
变得惰性化。 - tacaswellitertools.izip
,因为这甚至对他们来说都不是必需的,那将是愚蠢的。*尽管这也取决于我们。在我看来,Python3 至少已经准备好了(所以我们可以推动人们使用它),但我知道这不是唯一的“问题”... - jeromejmap(operator.mul, list1, list2)
。在这些非常简单的左侧表达式中,列表推导变得笨拙。 - Yann Vernier如果您计划编写任何异步、并行或分布式代码,您可能会更喜欢使用 map
而不是列表推导式——因为大多数异步、并行或分布式程序包都提供了一个 map
函数来重载 Python 的 map
。然后,通过将适当的 map
函数传递给剩下的代码,您可能不必修改原始串行代码就可以使其在并行环境中运行(等等)。
我发现列表推导式通常比map
更能表达我的意图 - 它们都可以完成任务,但前者可以节省尝试理解可能复杂的lambda
表达式所带来的心理负担。
还有一篇采访(我找不到它了)中,Guido列出了lambda
和函数式函数作为他最后悔接受Python的事情,因此你可以认为它们是不符合Python风格的。
const
关键字就是这方面的一个重大突破。 - Stuart Berg自Python 3开始,map()
是一个迭代器,你需要记住你需要什么:一个迭代器还是list
对象。
如@AlexMartelli已经 提到的那样,只有当你不使用lambda
函数时,map()
比列表推导式更快。
我将向你展示一些时间比较。
Python 3.5.2和CPython
我使用了Jupiter笔记本,特别是内置的%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()
!
我使用了perfplot来计时一些结果(这是我的一个项目)。
正如其他人所指出的那样,map
只返回一个迭代器,因此它是一个常数时间操作。当通过list()
来实现迭代器时,它与列表解析相当。根据表达式,其中一个可能略微占优势,但几乎没有太大的差异。
请注意,像x ** 2
等算术运算在NumPy中要快得多,特别是如果输入数据已经是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()
standalone_map
代码仅仅是实例化了一个 map
对象,并没有执行任何迭代 - 也就是说,六边形/正方形值的计算实际上从未发生。这当然解释了性能结果。在图表中包含这个并不太有用。 - Karl Knechtel我进行了一个快速测试,比较了三种调用对象方法的方法。在这种情况下,时间差异微不足道,这取决于所涉及的函数(请参见@Alex Martelli的 回答)。这里,我研究了以下方法:
1. 直接调用对象方法
2. 使用call()方法调用对象方法
3. 使用apply()方法调用对象方法
# 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中的int
)和浮点数(Python中的float
)列表(存储在变量vals
中)的递增列表大小。考虑下面的虚拟类DummyNum
:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
add
方法。在Python中,__slots__
属性是一种简单的优化方式,用于定义类(属性)所需的总内存,从而减小内存大小。map_comprehension
技术)对于对象中的两种添加类型都是最快的,尤其是对于较短的列表。[*map(f, vals)]
与[f(x) for x in vals]
)才能使map
更快。因此,list(map(methodcaller("add"), vals))
比[methodcaller("add")(x) for x in vals]
更快。当循环对应项使用不同的调用方法以避免一些开销时(例如,x.add()
避免了methodcaller
或lambda表达式的开销),map
可能不会更快。对于这个特定的测试用例,[*map(DummyNum.add, vals)]
将更快(因为DummyNum.add(x)
和x.add()
基本上具有相同的性能)。 - GZ0list()
比列表推导式稍微慢一些。为了公平比较,您需要编写 [*map(...)]
。 - GZ0list()
调用会增加负担。我应该花更多时间阅读答案。我将重新运行这些测试以进行公正比较,无论差异有多么微不足道。 - craymichael