列表推导式中的lambda函数为什么比map中的lambda函数慢?

4

我做了一个测试

%timeit list(map(lambda x:x,range(100000)))
%timeit [(lambda x:x)(i) for i in range(100000)]

提供

29 ms ± 4.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
51.2 ms ± 3.76 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

为什么在python中,lambda函数在列表推导式中比在map中执行速度慢?

请注意,“map”调用等同于您的推导式实际上是“list(map(lambda i: (lambda x:x)(i),range(100000)))”。 - MisterMiyagi
2个回答

7
因为在第一段代码片段中,lambda只被创建一次并执行100000次,而在第二段中,它在每次迭代中都被创建和执行。
说实话,我很惊讶差距不是更大,但你的两个时间应该会随着可迭代对象的长度越来越大而有更大的差别。
另外需要注意的是,即使将lambda更改为内置函数,这个函数不需要先被创建,而只需要查找,你仍然会得到相同的趋势:
> py -m timeit "list(map(float, range(10000)))"
200 loops, best of 5: 1.78 msec per loop

> py -m timeit "[float(x) for x in range(10000)]"
100 loops, best of 5: 2.35 msec per loop

将函数绑定到变量可以改善性能,但是list(map())方案仍然更快。

> py -m timeit "r=float;[r(x) for x in range(10000)]"
100 loops, best of 5: 1.93 msec per loop

至于为什么绑定到本地变量更快,您可以在这里找到更多信息。


2
你能澄清一下最后一段吗?使用“float”之所以更慢,原因完全相同——即“list(map())”与“列表推导式”只构建表达式一次与每次构建的区别。只是差别小得多,因为构建“lambda”比查找“builtin”要昂贵得多。 - MisterMiyagi
@MisterMiyagi,您说得完全正确。我会进行编辑。 - Ma0
@MisterMiyagi 哇,这真是出乎意料。网络上有很多地方都推崇列表推导式。所以实际上,“map”在函数映射方面具有性能优势。 - user15964
@Ev.Kounis 你好,感谢你的回答。顺便问一下,我不明白为什么 r=float 会稍微提高列表推导速度(虽然仍然比 map 慢)?我认为它仍然需要查找。为什么会有差异呢? - user15964
@user15964,因为r是一个局部变量,而float是一个内置的可以被覆盖的变量。因此,在查找时需要更多的查找次数来找到float,而在搜索locals时会首先查找r - Ma0

3

TLDR: comprehension 在每次评估其整个表达式。map 只在第一次评估表达式,并在之后的每次使用结果


map 和 comprehension 处理传入的表达式方式不同。 可以将 map 和 comprehension 的关键部分翻译成如下方式:

def map(func: 'callable', iterable):
    for item in iterable:
        yield func(item)

def comprehension(expr: 'code', iterable):
    for item in iterable:
        yield eval(expr)

重要的区别是map已经接收了一个函数,而comprehension接收一个表达式
现在,lambda x:x是一个求值为函数的表达式。
>>> co = compile('lambda x: x', '<stackoverflow>', 'eval')
>>> co
<code object <module> at 0x105d1c810, file "<stackoverflow>", line 1>
>>> eval(co)
<function __main__.<lambda>(x)>

值得注意的是,评估表达式以获取函数是需要时间的操作。

由于map和推导式期望不同的内容,因此lambda被以不同的方式传递给它们。解释器所做的可以通过显式的eval/compile进行扩展:

>>> list(map(eval(compile('lambda x: x', '<stackoverflow>', 'eval')), range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(comprehension(compile('(lambda x: x)(item)', '<stackoverflow>', 'eval'), range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

重要的区别现在应该清楚了:
- 对于 `map`,表达式在传递到 `map` 之前只被计算一次。对于每个项目,`map` 调用生成的函数。 - 对于推导式,表达式未经计算即被传递。对于每个项目,推导式构造函数,然后调用生成的函数。
需要注意的是,`map` 并不总是比推导式更快。`map` 的好处在于只需创建/查找一次函数。但是,函数调用很费资源,而且推导式不必使用函数。
通常情况下,如果表达式仅使用推导式内部的本地变量,则速度会更快。
In [302]: %timeit list(map(lambda i:i,range(100000)))
     ...: %timeit [i for i in range(100000)]
8.86 ms ± 38.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
4.49 ms ± 35.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

如果表达式本身必须动态执行一项昂贵的操作,则map无法产生好处。
In [302]: %timeit list(map(lambda i: (lambda x:x)(i),range(100000)))
     ...: %timeit [(lambda x:x)(i) for i in range(100000)]
19.7 ms ± 39.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
15.9 ms ± 43.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

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