我查看了每个构造的反汇编代码(使用dis)。我通过声明这两个函数来完成:
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
。
对于列表推导式:
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
对于生成器表达式:
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
实际求和的反汇编代码如下:
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>
vs list_comprehension.<locals>.<listcomp>
(因此只是加载了不同的本地变量)。
前两个反汇编之间的不同字节码指令是列表推导式的 LIST_APPEND
vs. 生成器表达式的 YIELD_VALUE
和 POP_TOP
的连接。
我不会假装我知道 Python 字节码的内在机制,但我从中得出的结论是生成器表达式被实现为一个队列,在生成值后弹出。这种弹出不必发生在列表推导中,这使我相信使用生成器将会有一些开销。
现在这并不意味着生成器总是会更慢。生成器擅长于节省内存,因此会有阈值 N,使得在此阈值之前列表推导性能略优(因为内存使用不成问题),但在此阈值之后,生成器将显著表现更好。