如何评价生成器比推导式快?

2021-07-25 02:13:31 +08:00
 O5oz6z3
谢邀。
直觉上,连串的迭代的话,应该是生成器快。因为生成器是惰性求值,不用像推导式经过中间变量容器的步骤。

但是用空间时间的角度去看,应该是推导式快,因为推导式空间换时间,生成器时间换空间。

还是说要视乎情况?比如简单迭代推导式更快,连串迭代生成器更快?

开题一句话,内容全靠编。
一个细枝末节的小问题,不知道大家有什么看法?
(问题上下文是 Python,主要是 py3+)

补充,推导式是 [x for x in iter],生成器是 (x for x in iter) 或者 yield 。
不讨论语法上哪个能容纳更复杂逻辑的问题。
2965 次点击
所在节点    Python
20 条回复
littleMaple
2021-07-25 05:06:22 +08:00
这种原生语法的速度比较需要讨论到底层实现,生成器要维护一个底层的协程,推导式不用,这是我能想到的其中一个对速度有影响的区别。
O5oz6z3
2021-07-25 06:29:55 +08:00
@littleMaple 确实还有这个因素,也就是说运行开销生成器更重一些,推导式更轻一些。至于底层实现我就不太清楚了……
Richard14
2021-07-25 06:52:56 +08:00
评价:理所当然
O5oz6z3
2021-07-25 08:43:30 +08:00
@Richard14 好吧,看来是我问得不够具体。虽然我也觉得显而易见,但是纸上谈兵不够肯定。比如说考虑到运行开销,连串嵌套的生成器应该慢一些;考虑到内存分配速度,也许是推导式慢一些。
虽然可能归根到底,这两者的速度差距其实无关紧要。
renmu123
2021-07-25 10:05:12 +08:00
这两者主要是内存上的差异吧
billlee
2021-07-25 15:55:40 +08:00
python 就不要考虑性能了吧
WilliamYang
2021-07-25 18:14:35 +08:00
@billlee 完全同意,现在还考虑 Python 怎么写性能好些,真的觉得像是很无聊的事
O5oz6z3
2021-07-25 19:42:30 +08:00
@renmu123 同意,只是突然想到可以将速度也作为生成器和推导式的其中一种区别,所以抛出这么一个问题。


@billlee @WilliamYang 确实,不知道有多少人经常拿动态语言和静态语言比……
abersheeran
2021-07-25 20:39:41 +08:00
In [1]: import dis

In [2]: dis.dis("[x for x in iter]")
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x000001F48BAC25D0, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (iter)
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x000001F48BAC25D0, file "<dis>", line 1>:
1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 8 (to 14)
6 STORE_FAST 1 (x)
8 LOAD_FAST 1 (x)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 4
>> 14 RETURN_VALUE

In [3]: dis.dis("(x for x in iter)")
1 0 LOAD_CONST 0 (<code object <genexpr> at 0x000001F48C359030, file "<dis>", line 1>)
2 LOAD_CONST 1 ('<genexpr>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (iter)
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE

Disassembly of <code object <genexpr> at 0x000001F48C359030, file "<dis>", line 1>:
1 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 10 (to 14)
4 STORE_FAST 1 (x)
6 LOAD_FAST 1 (x)
8 YIELD_VALUE
10 POP_TOP
12 JUMP_ABSOLUTE 2
>> 14 LOAD_CONST 0 (None)
16 RETURN_VALUE
O5oz6z3
2021-07-25 23:12:06 +08:00
@abersheeran
虽然看不懂,就指令的数量上来看,两者似乎没有差别。看起来性能差距微乎其微,初步测试了一下结果是生成器输了……
没想到 dis.dis 的递归反汇编功能要到 3.7 才出现。
O5oz6z3
2021-07-25 23:16:56 +08:00
@O5oz6z3 #10
临时也想不出什么比较好的用来测试的逻辑:
import timeit
_iter = list(range(12345))
init = 'iter = _iter'
end = 'iter = list(iter)'
maketest = lambda stmt, n: '\n'.join([init, *[stmt]*n, end])
O5oz6z3
2021-07-25 23:19:38 +08:00
(测试)
comp = 'iter = [x**0.5 for x in iter]'
timeit.timeit(maketest(comp, 3), number=100, globals=globals())
O5oz6z3
2021-07-25 23:20:46 +08:00
@O5oz6z3 #10
import timeit
_iter = list(range(12345))
init = 'iter = _iter'
end = 'iter = list(iter)'
maketest = lambda stmt, n: '\n'.join([init, *[stmt]*n, end])

comp = 'iter = [x**0.5 for x in iter]'
>>> timeit.timeit(maketest(comp, 3), number=100, globals=globals())
1.33
>>> timeit.timeit(maketest(comp, 10), number=100, globals=globals())
4.26

gene = 'iter = (x**0.5 for x in iter)'
>>> timeit.timeit(maketest(gene, 3), number=100, globals=globals())
1.63
>>> timeit.timeit(maketest(gene, 10), number=100, globals=globals())
4.60
fakepoet
2021-07-26 12:33:08 +08:00
我觉得这是一个非常有趣的问题,花了点时间搜索了一下,stackoverflow 上的[这个回答]( https://stackoverflow.com/questions/16307326/why-this-list-comprehension-is-faster-than-equivalent-generator-expression)做出的解释看上去可能性比较大,简而言之就是从 Bytecode 中看两者的差异主要是`POP_TOP`这条指令,所以怀疑生成器底层有队列的实现维护生成好的值,而 pop 操作是导致花费更多运行时间的原因。
fakepoet
2021-07-26 12:33:44 +08:00
(我是真的不会用 v2 的 markdown...
zachlhb
2021-07-26 18:14:14 +08:00
说的示例不都是推导式吗,一个是列表推导式,一个是元组推导式
jaredyam
2021-07-26 23:10:29 +08:00
O5oz6z3
2021-07-27 00:32:48 +08:00
@fakepoet #14 我也觉得很有趣,虽然实际意义不大……
底层队列什么的我就不懂了,不过我的看法也和链接里的一样:通常情况下内存不是瓶颈,所以速度上推导式略胜一筹,但其实两者的差距非常小可以忽略不计。也就是推导式速度不是快很多,但生成器非常省内存。

@zachlhb #16 如果我没搞错的话,所谓的元组推导式正式名称就是生成器表达式,虽然生成器不止这一种写法。

@fakepoet @jaredyam #15 (可能这是 V2EX 特色,让用户在每次回复时摸索出真正的语法,增加趣味性)

顺带一提,#13 楼的写法又抛出了另一个问题:生成器表达式中的 iter 到底指向的是当时的值还是说引用全局标识符 iter ?如果是后者那么就乱套了……
imn1
2021-07-31 15:16:55 +08:00
生成器主要是语句执行时并不运算,而是后面调用的时候才一并运算,推导式是语句执行同时计算相关值
例如有一个列表,变形得出相应值,但是后续是根据 if 分支是否使用这些值,这时候用生成器比较好,需要时才计算;在不需要时这个生成器并不会耗费太多时间和算力;又或者类似地两个列表,后续根据 if 二选一,两个都直接计算的话,其中一个必然浪费时间和算力,用生成器就省一些

#18
local 变量优先,如果本地变量名和一些全局名称相同的话,会使用本地变量名,当然有部份关键字是不能用于命名的
例如
list = (1,2,3)
list(range(5)) # 这句就会报错

@zachlhb #16
元组推导式是 tuple(x for x in iter)
O5oz6z3
2021-07-31 17:41:04 +08:00
@imn1 感谢回复……
关于 #18 楼提到的 #13 楼的问题,是我说得太含糊了,让人理解成本地变量名覆盖全局内置函数名称的软关键字问题。机会难得我就顺便补充一下。

举个例子:
iter = [1, 2, 3, 4]
iter = (x+2 for x in iter)
iter = (x+3 for x in iter)
以上代码中,第三行右侧的 `(x+3 for x in iter)` 表达式中的 iter 迭代时会展开为第二行右侧的生成器表达式 `(x+2 for x in iter)`。问题就是此时第二行右侧生成器中的 iter 指的是哪个 iter ?是 `(x+2 for x in iter)` 还是第一行的 [1, 2, 3, 4]?

后来实际验证了一下,答案是第一行的 [1, 2, 3, 4],也就是生成器表达式中的 iter 指向定义时候的 iter 变量的值,而不是执行的时候动态引用作用域中 iter 变量的值。有点像是闭包保存了定义时候的作用域。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/791572

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX