Python: 列表推导式 vs. map函数

14

参考这个Python List Comprehension Vs. Map问题,有人可以解释一下为什么当列表推导式不调用函数时,它比map表现更好,即使map中没有lambda函数,但当调用函数时,它却给出了最差的结果吗?

import timeit

print timeit.Timer('''[i**2 for i in xrange(100)]''').timeit(number = 100000)

print timeit.Timer('''map(lambda i: i**2, xrange(100))''').timeit(number = 100000)

print timeit.Timer(setup="""def my_pow(i):
    return i**2
""",stmt="""map(my_pow, xrange(100))""").timeit(number = 100000)

print timeit.Timer(setup="""def my_pow(i):
    return i**2
""",stmt='''[my_pow(i) for i in xrange(100)]''').timeit(number = 100000)

结果:

1.03697046805 <-- list comprehension without function call
1.96599485313 <-- map with lambda function
1.92951520483 <-- map with function call
2.23419570042 <-- list comprehension with function call

无论在map中调用的函数是lambda还是常规函数,开销仍然存在。不知道为什么带有函数调用的列表推导式比map()慢。 - millimoose
@millimoose 但是lambda函数会在每次迭代时声明,这会有任何变化吗? - zenpoy
4
函数调用参数在函数被调用之前进行求值,因此该函数仅声明一次。 - Sven Marnach
1
@SvenMarnach我觉得他/她在说的是my_pow的定义只被解释一次,而lambda表达式在每次迭代中都会被定义。这是一个有效的问题,也许lambda表达式导致它的版本稍微慢一些。 - soulcheck
@millimoose 嗯,可以说在 OP 的问题中,使用 lambda 函数和函数调用的 map 之间的差异是微不足道的。 - soulcheck
显示剩余2条评论
1个回答

15

所有的时间结果都可以通过以下事实来解释:

  1. CPython有相当高的函数调用开销。

  2. map(f, it)[f(x) for x in it] 稍微快一点。

你的代码的第一个版本根本没有定义函数,所以没有函数调用开销。第二个版本需要定义函数,因此每次迭代都会有函数调用开销。第三个版本与第二个版本完全相同 - 函数和lambda表达式是相同的东西。根据事实2,最后一个版本甚至更慢。


3
我能想到关于事实(2)的两个可能原因:map 循环直接在 C 中执行,因此在循环中具有稍微较少的开销,而对于列表推导式,名称 f 的引用需要在每次迭代中解析一次,但对于 map 来说只需要解析一次。我认为第二个原因更为重要,但有什么方法可以确定哪个原因更具贡献性吗? - Danica
@Dougal:这不是第一次有人提到在C语言中进行循环,但我从未找到过任何参考资料或证据来支持它应该更快的说法。 - Rik Poggi
@RikPoggi 我尝试检查了Python源代码,发现在3.x中map实际上返回一个迭代器,并且使用参数的迭代器进行操作,这使得那个理论相当不可能。虽然所有这些都是基于C的,但仍然可能存在一些节省。实际上,比较2.x和3.x在这方面的性能可能会很有趣(即使没有任何结论)。 - millimoose
@SvenMarnach 说得好,但这个区别会有显著的差别吗?运行时仍然需要处理操作解释器状态的所有开销;在我看来,字节码调度本身似乎不是减速的主要 contributor。 - millimoose
@millimoose:我肯定没有这样的误解,而且我认为我同意你说的一切。我只是想澄清当人们说“这个循环完全在C代码中”时通常指什么。(所谓“人们”,我指的是熟悉CPython源代码的人。) - Sven Marnach
显示剩余3条评论

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