如何评价生成器比推导式快? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
O5oz6z3
V2EX    Python

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

  •  
  •   O5oz6z3 2021-07-25 02:13:31 +08:00 3549 次点击
    这是一个创建于 1589 天前的主题,其中的信息可能已经有所发展或是发生改变。
    谢邀。
    直觉上,连串的迭代的话,应该是生成器快。因为生成器是惰性求值,不用像推导式经过中间变量容器的步骤。

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

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

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

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


    @billlee @WilliamYang 确实,不知道有多少人经常拿动态语言和静态语言比……
    abersheeran
        9
    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
        10
    O5oz6z3  
    OP
       2021-07-25 23:12:06 +08:00
    @abersheeran
    虽然看不懂,就指令的数量上来看,两者似乎没有差别。看起来性能差距微乎其微,初步测试了一下结果是生成器输了……
    没想到 dis.dis 的递归反汇编功能要到 3.7 才出现。
    O5oz6z3
        11
    O5oz6z3  
    OP
       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
        12
    O5oz6z3  
    OP
       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
        13
    O5oz6z3  
    OP
       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
        14
    fakepoet  
       2021-07-26 12:33:08 +08:00   1
    我觉得这是一个非常有趣的问题,花了点时间搜索了一下,stackoverflow 上的[这个回答]( https://stackoverflow.com/questions/16307326/why-this-list-comprehension-is-faster-than-equivalent-generator-expression)做出的解释看上去可能性比较大,简而言之就是从 Bytecode 中看两者的差异主要是`POP_TOP`这条指令,所以怀疑生成器底层有队列的实现维护生成好的值,而 pop 操作是导致花费更多运行时间的原因。
    fakepoet
        15
    fakepoet  
       2021-07-26 12:33:44 +08:00
    (我是真的不会用 v2 的 markdown...
    zachlhb
        16
    zachlhb  
       2021-07-26 18:14:14 +08:00 via iPhone
    说的示例不都是推导式吗,一个是列表推导式,一个是元组推导式
    jaredyam
        17
    jaredyam  
       2021-07-26 23:10:29 +08:00
    O5oz6z3
        18
    O5oz6z3  
    OP
       2021-07-27 00:32:48 +08:00
    @fakepoet #14 我也觉得很有趣,虽然实际意义不大……
    底层队列什么的我就不懂了,不过我的看法也和链接里的一样:通常情况下内存不是瓶颈,所以速度上推导式略胜一筹,但其实两者的差距非常小可以忽略不计。也就是推导式速度不是快很多,但生成器非常省内存。

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

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

    顺带一提,#13 楼的写法又抛出了另一个问题:生成器表达式中的 iter 到底指向的是当时的值还是说引用全局标识符 iter ?如果是后者那么就乱套了……
    imn1
        19
    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
        20
    O5oz6z3  
    OP
       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 变量的值。有点像是闭包保存了定义时候的作用域。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     817 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 20:22 PVG 04:22 LAX 12:22 JFK 15:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86