使用map()和for循环有何区别?是否有优劣之分?

10

map() 方法是否像 "for" 循环一样遍历列表?使用 map() 是否有优势?

如果是这样,那么我的代码现在看起来像这样:

for item in items:
    item.my_func()

如果有意义的话,我想将它转换为map()函数。这可行吗?你能给个例子吗?

9个回答

24
你可以使用map代替你展示的for循环,但由于你似乎没有使用item.my_func()的结果,所以不建议这样做。如果你想对列表的所有元素应用一个没有副作用的函数,应该使用map。在其他情况下,请使用显式的for循环。
另外,从Python 3.0开始,map返回一个迭代器,所以在这种情况下,map的行为将不同(除非你显式地评估迭代器返回的所有元素,例如通过调用list)。

编辑: kibibu 在评论中要求对为什么 map 的第一个参数不应该是具有副作用的函数进行澄清。我将尝试回答这个问题:

map 应该被传递一个函数 f在数学意义上。在这种情况下,不管 f 被应用于第二个参数的元素的顺序如何(当然,它们需要以原始顺序返回),都不重要。更重要的是,在这种情况下,map(g, map(f, l)) 在语义上等同于 map(lambda x: g(f(x)), l)不论 fg 在其各自输入上被应用的顺序如何

例如,不管 map 是一次返回迭代器还是完整列表都无关紧要。然而,如果 f 和/或 g 会引起副作用,那么只有在 map(f, l) 的语义是这样的:在任何阶段,在 map(f, l)f 应用于 l 的第 *(n + 1)*​ 个元素之前,g 被应用于 map(f, l) 返回的前 n 个元素。(这意味着 map 必须进行尽可能惰性的迭代——Python 3 中是这样做的,但 Python 2 不是!)

更进一步地说:即使我们假设了 Python 3 版本的 map 实现,如果 map(f, l) 的输出经过 itertools.tee 处理后再提供给外部的 map 调用,语义等价性也很容易破坏。

上述讨论可能看起来很理论化,但随着程序变得更加复杂,它们变得更难以推理,因此调试也更困难。确保某些事物是不变的在一定程度上缓解了这个问题,并且可能实际上预防了一整类错误。
最后,`map` 让很多人想起在各种(纯)函数式语言中的真正函数式对应物。将具有副作用的“函数”传递给它会让这些人感到困惑。因此,鉴于另一种选择(即使用显式循环)与调用 `map` 的难度相当,强烈建议只在函数的应用不引起副作用的情况下限制使用 `map`。

为什么不推荐使用它?是语义问题,还是某种分布式处理问题(其中副作用会破坏并行性)? - kibibu
1
@kibibu:我开始回答你的问题,但最终得到的文本太多了,无法在评论框中容纳。所以我更新了答案 :) 希望这有所帮助!(至于你关于可迭代对象的分布式处理的评论:虽然在其他语言中这可能被引用为谈论map的额外论点,但我不认为Python(目前)执行任何此类优化。) - Stephan202

5
您可以像这样使用映射来编写代码:

您可以像这样使用映射来编写代码:

map(cls.my_func, items)

将cls替换为您正在迭代的项目的类。

正如Stephan202所提到的那样,在这种情况下不建议这样做。

一般来说,如果您想通过将某个函数应用于列表中的每个项目来创建一个新的列表,请使用map。这意味着函数没有副作用,因此您可以(潜在地)并行运行map。

如果您不想创建新列表,或者函数具有副作用,请使用for循环。这是您示例中的情况。


2

这里存在微小的语义差异,这可能在Python语言规范中已经解决。 Map 可以显式地并行化,而 for 只能在特殊情况下并行化。代码可以从 for 中跳出,但只能通过异常从 map 中逃脱。

我认为 map 不应该保证函数应用的顺序,而 for 必须保证。据我所知,目前没有Python实现能够自动并行化。


2

如果需要,您可以将您的map切换成一些很棒的线程化、多进程或分布式计算框架。例如,Disco是一个基于Erlang和Python的分布式框架,具有抗故障性。我在两个8核的计算机上配置了它,现在我的程序运行速度快了16倍,这要感谢Disco集群,但是我不得不从列表推导和for循环重写程序到map/reduce。

使用for循环和列表推导和使用map/reduce编写程序没有什么区别,但是当您需要在集群上运行时,如果使用map/reduce,您几乎可以免费完成它。如果没有使用,那么您将不得不进行重写。

注意:据我所知,Python 2.x从map中返回列表而非迭代器。我听说这可以通过使用iter.imap()来避免(虽然我从未使用过)。


2

如果您不需要返回结果列表(例如,具有副作用的函数),请使用显式for循环。

如果您需要返回结果列表(例如,基于输入直接返回值的函数),请使用列表推导式。

如果您想说服Lisp用户使用Python,请使用map()。


1

map 的主要优势在于当您想要获取列表中每个元素的某些计算结果时。例如,此代码段将列表中的每个值都加倍:

map(lambda x: x * 2, [1,2,3,4])  #=> [2, 4, 6, 8]

需要注意的是,map 返回一个包含结果的新列表。它不会直接修改原始列表。

如果使用 for 来完成相同的操作,则需要创建一个空列表,并在 for 循环体中添加一行代码将每次计算的结果添加到新列表中。使用 map 更加简洁和实用。


4
列表解析更加简洁,例如在你的例子中,[x*2 for x in [1,2,3,4]]。 - Kiv

0

对于内置函数,使用Map有时比手动编写for循环更快。尝试计时map(str, range(1000000))和类似的for循环。


0

已经有许多好答案了。我想强调map()的另一个方面:代码的可读性和透明度。

特别是,我将专注于问题中的这部分内容:

使用mapfor相比是否有价值?


对我来说,在适当的情况下,使用map()而不是循环或推导式,主要的好处是使代码更加透明。
(为简单起见,下面假设我们在一个list上操作,并且我假装map()返回一个list。[1])
对于阅读或浏览代码的人来说,通过map()转换的列表可以给读者提供额外的信息,并且非常廉价地实现这一点。这些信息分为两类:
  • 由实现强制执行的
  • 传达意图的
使用map(),而不是循环或推导式,
  • 它被强制执行(我们确定):
    • 生成的列表与输入列表具有相同数量的元素
    • 输入列表没有发生变化
  • 程序员传达了他们的意图:
    • 应用的函数没有副作用
请注意,在Python社区中,通常认为使用推导式或for循环比使用map()更符合Python的风格。
[1]:map()的返回类型和惰性求值
map()接受一个可迭代对象,并返回一个类似生成器的可迭代对象。
可迭代对象的示例包括列表和元组。
您可以将返回值视为迭代器,但该值只在需要时进行评估。这是因为map()实际上返回了一个生成器,这被称为惰性求值。
我们可以通过一个名为prnt()的函数来说明这种行为,该函数返回None,并具有将其参数打印到控制台的副作用:
>>> def prnt(x): print(f'printing {x}'); return None
>>> a = prnt(1)
printing 1
>>> print(a)
None

现在,
# list comprehension:
>>> c = [prnt(x) for x in [1,2]]
printing 1
printing 2
>>> print(c)
[None, None]

# map:
>>> b = map(prnt, [1,2])
# no output - prints nothing!
>>> b  # what is it?
<map at 0x11241e290>
>>> l=list(b)  # converting to list forces evaluation
printing 1
printing 2
>>> l
[None, None]

-3
map(lambda item: item.my_func(), items)

1
这确实可行,但并没有回答为什么这比使用for循环更好的问题(即它并不比for循环更好)。 - Kiv

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