关于python:为什么求和列表理解比生成器表达式快?

Why is summing list comprehension faster than generator expression?

不确定标题是否是正确的术语。

如果你必须比较2个字符串(A,B)中的字符并计算B中字符与A的匹配次数:

1
sum([ch in A for ch in B])

在 %timeit 上比

1
sum(ch in A for ch in B)

我知道第一个将创建一个 bool 列表,然后对 1 的值求和。
第二个是发电机。我不清楚它在内部做什么以及为什么它变慢了?

谢谢。

使用 %timeit 结果进行编辑:

10 个字符

生成器表达式

列表

10000 个循环,最好的 3 个:每个循环 112 μs

10000 个循环,最好的 3 个:每个循环 94.6 μs

1000 个字符

生成器表达式

列表

100 个循环,最好的 3 个:每个循环 8.5 毫秒

100 个循环,最好的 3 个:每个循环 6.9 毫秒

10,000 个字符

生成器表达式

列表

10 个循环,最好的 3 个:每个循环 87.5 毫秒

10 个循环,最好的 3 个:每个循环 76.1 毫秒

100,000 个字符

生成器表达式

列表

1 个循环,最好的 3 个:每个循环 908 毫秒

1 个循环,最好的 3 个:每个循环 840 毫秒


我查看了每个构造的反汇编(使用 dis)。我通过声明这两个函数来做到这一点:

1
2
3
4
5
def list_comprehension():
    return sum([ch in A for ch in B])

def generation_expression():
    return sum(ch in A for ch in B)

然后用每个函数调用 dis.dis

对于列表理解:

1
2
3
4
5
6
7
8
9
10
 0 BUILD_LIST               0
 2 LOAD_FAST                0 (.0)
 4 FOR_ITER                12 (to 18)
 6 STORE_FAST               1 (ch)
 8 LOAD_FAST                1 (ch)
10 LOAD_GLOBAL              0 (A)
12 COMPARE_OP               6 (in)
14 LIST_APPEND              2
16 JUMP_ABSOLUTE            4
18 RETURN_VALUE

和生成器表达式:

1
2
3
4
5
6
7
8
9
10
11
 0 LOAD_FAST                0 (.0)
 2 FOR_ITER                14 (to 18)
 4 STORE_FAST               1 (ch)
 6 LOAD_FAST                1 (ch)
 8 LOAD_GLOBAL              0 (A)
10 COMPARE_OP               6 (in)
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE            2
18 LOAD_CONST               0 (None)
20 RETURN_VALUE

实际求和的反汇编为:

1
2
3
4
5
6
7
8
9
 0 LOAD_GLOBAL              0 (sum)
 2 LOAD_CONST               1 (<code object <genexpr> at 0x7f49dc395240, file"/home/mishac/dev/python/kintsugi/KintsugiModels/automated_tests/a.py", line 12>)
 4 LOAD_CONST               2 ('generation_expression.<locals>.<genexpr>')
 6 MAKE_FUNCTION            0
 8 LOAD_GLOBAL              1 (B)
10 GET_ITER
12 CALL_FUNCTION            1
14 CALL_FUNCTION            1
16 RETURN_VALUE

但是这个 sum 反汇编在你的两个例子中是不变的,唯一的区别是 generation_expression.<locals>.<genexpr>list_comprehension.<locals>.<listcomp> 的加载(所以只加载一个不同的局部变量)。

前两次反汇编之间的不同字节码指令是 LIST_APPEND 用于列表理解,而 YIELD_VALUEPOP_TOP 的连接用于生成器表达式。

我不会假装我知道 Python 字节码的内在原理,但我从中得出的结论是生成器表达式是作为队列实现的,在队列中生成值然后弹出。这种弹出不一定发生在列表推导式中,这让我相信使用生成器会产生少量开销。

现在这并不意味着生成器总是会变慢。生成器在内存效率方面表现出色,所以会有一个阈值 N,使得列表理解在这个阈值之前会表现得稍微好一些(因为内存使用不会成为问题),但是在这个阈值之后,生成器的表现会明显更好。


生成器通常比列表理解慢,生成器的重点是提高内存效率,因为它们通过以惰性方式(仅在实际需要时)创建每个项目来生成每个项目。他们更喜欢内存效率而不是速度。