何时应该使用Map而不是For循环?

80

以下内容涉及以下代码:(使用Python语言)

for i in object:
     doSomething(i)

对比

map(doSomething, object)

两种方式都易于理解而且简短,但它们之间有速度差异吗?如果doSomething函数有一个需要检查的返回值,那么在map中它将被作为列表返回,在for循环中我们可以创建自己的列表或逐个检查。

for i in object:
     returnValue = doSomething(i)
     doSomethingWithReturnValue(returnValue)

对比

returnValue = map(doSomething, object)
map(doSomethingWithReturnValue, returnValue)

现在,我感觉这两个函数有点分歧。基于我们在循环中逐步检查它们还是在最后一次性检查它们,这两个doSomethingWithReturnValue函数可能会有所不同。此外,for循环似乎总是有效的,虽然速度可能较慢,而map只在特定情况下有效。当然,我们可以扭曲其中一个来使其工作,但整个重点是避免这种类型的工作。

我想要的是一个场景,在这种情况下,映射函数确实比做得好的for循环在性能、可读性、可维护性或实现速度方面表现更出色。如果答案是真的没有太大区别,那么我想知道人们什么时候使用其中之一,或者是否完全是随机的,并根据你的机构的编码标准进行设置。

谢谢!


3
你可以使用列表/字典/集合推导式或生成器来替代map函数 - 这取决于doSomething()函数的作用。 - Hamish Grubijan
2
不要试图在初期调整性能。我总是会选择最易读的选项。运行时稍后会显示是否存在阻碍性能的问题,如果需要提高速度或资源使用率,那么再进行优化。 - manuel aldana
5个回答

52

map函数在处理可迭代对象时很有用,它会将函数应用到每个元素上并返回结果的列表。使用map相比使用for循环并构造列表更为简单和简洁。

for循环通常在其他情况下更容易阅读,在Lisp中,有很多迭代构造函数基本上是使用宏和map编写的。因此,在不适合使用map的情况下,请使用for循环。

理论上,如果我们有一个足够聪明以利用多个CPU /处理器的编译器/解释器,那么map可以更快地实现,因为可以并行地执行每个元素上的不同操作。然而,我认为目前情况并非如此。


21
为什么要用过去时?Lisp 依然活跃并广泛使用。 - Svante
5
实际上,即使在单线程中,map在性能方面也要优于for,因为循环是用C语言编写的。请参阅我的帖子获取速度测试结果。 - iamamac
4
一般来说,地图函数不能自动并行化,因为每个函数调用可能具有全局副作用。在 Python 2.6+ 中已经存在一个并行地图:multiprocessing.Pool.map()。 - Eric O. Lebigot
1
多进程.Pool.map? - baconwichsand
1
你可以使用 concurrent.futures 和它的 map() 函数在 Python 3 或 Python 2 中实现并行化。以下是 Python 3 的文档(包含一个很好的 ProcessPoolExecutor 示例)。https://docs.python.org/3/library/concurrent.futures.html - Nathaniel Givens
显示剩余5条评论

26

你是否熟悉timeit模块?下面是一些时间记录。-s执行一次性的设置,然后循环执行命令并记录最佳时间。

1> python -m timeit -s "L=[]; M=range(1000)" "for m in M: L.append(m*2)"
1000 loops, best of 3: 432 usec per loop

2> python -m timeit -s "M=range(1000);f=lambda x: x*2" "L=map(f,M)"
1000 loops, best of 3: 449 usec per loop

3> python -m timeit -s "M=range(1000);f=lambda x:x*2" "L=[f(m) for m in M]"
1000 loops, best of 3: 483 usec per loop

4> python -m timeit -s "L=[]; A=L.append; M=range(1000)" "for m in M: A(m*2)"
1000 loops, best of 3: 287 usec per loop    

5> python -m timeit -s "M=range(1000)" "L=[m*2 for m in M]"
1000 loops, best of 3: 174 usec per loop

请注意,除了最后两个之外,它们都很相似。正是函数调用(L.append或f(x))严重影响了计时。在#4中,L.append查找已经在设置中完成了一次。在#5中,使用了一个没有函数调用的列表综合。


我认为你所指的是我的帖子。是的,在 py3k 中,我发现了 map 返回迭代器的严重问题,但我不认为 timeit 存在任何问题,因为 range 返回迭代器,因此不将其放入设置阶段没有什么影响。 - iamamac
python3 -m timeit "[m for m in range(1000)]"3次中最好的结果:114微秒每循环,共进行了10000次循环。python3 -m timeit -s M=list(range(1000)) "[m for m in M]"3次中最好的结果:83微秒每循环,共进行了10000次循环。构建列表只需一次,这会产生显著的差异。 - Mark Tolonen

11

只需使用列表理解:它们更具Python风格。它们的语法与生成器表达式类似,这使得从一个转换到另一个变得容易。在将代码转换为py3k时,您无需更改任何内容:map在py3k中返回可迭代对象,您需要调整您的代码。

如果您不关心返回值,只需不给新列表命名;当您在代码中需要使用返回值时,您可以切换到生成器表达式和仅用一行的列表推导式。


4

这里有一个关于循环和映射的实验,旨在展示哪个更有效:

from time import time

fruit  = range(4_000_000)

tic = time()
res=[True if i % 2 == 0 else False for i in fruit]
toc = time()
print(toc - tic)


tic = time()
list(map(lambda s: s % 2 == 0, fruit))
toc = time()
print(toc - tic)

tic = time()
res = []
for i in fruit:
    if i % 2 == 0:
        res.append(True)
    else:
        res.append(False)
toc = time()
print(toc - tic)

输出:

0.29710841178894043
0.4369466304779053
0.5959873199462891

因此,第一种方法是最有效的方法。


0

编辑:我没有意识到在 Python 3.0 之后,map 等于 itertools.imap。因此这里的结论可能不正确。我明天会在 Python 2.6 上重新运行测试并发布结果。

如果 doSomething 很“微小”,那么 map 可能比 for 循环或列表推导式快得多:

# Python 3.1.1 (r311:74483, Aug 17 2009, 17:02:12) [MSC v.1500 32 bit (Intel)] on win32

from timeit import timeit

do = lambda i: i+1

def _for():
  for i in range(1000):
    do(i)

def _map():
  map(do, range(1000))

def _list():
  [do(i) for i in range(1000)]

timeit(_for, number=10000)  # 2.5515936921388516
timeit(_map, number=10000)  # 0.010167432629884843
timeit(_list, number=10000) # 3.090125159839033

这是因为map是用C语言编写的,而for循环和列表推导式则在Python虚拟机中运行。


1
我不知道你的数字是从哪里获取的,但在我的情况下(Python 2.6),for循环更快,大约快5%。你的代码甚至都不正确,因为_list执行的迭代次数更少。你得到的巨大差异表明你的设置存在严重问题。 - interjay
这太荒谬了。你的代码根本不等价。看看我的答案。即使没有 map 对象与列表二分法,你的代码也是不同的,真遗憾你看不出来。 - SilentGhost
抱歉,当我复制代码并进行格式化时发生了一些拼写错误。我在我的Core Duo 4300 PC上运行Python 3.1.1,map显著击败了其他两个选项。 - iamamac
2
我在Python 2.6.4中运行了这段代码,迭代了100000次,结果得到了大约15个_for,18个_map和17个_list。 - indiv
7
地图返回一个生成器,这就是为什么它如此快速...执行list(_map(..)),这将是实时的。 - Tolo Palmer
-1 Tolo是正确的,你正在返回一个生成器,但它很快就不会被使用,所以当然是快的。由于这是完全错误的,因此会取消点赞。 - Matt Messersmith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接