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 · 3103 次点击
    这是一个创建于 1210 天前的主题,其中的信息可能已经有所发展或是发生改变。
    谢邀。
    直觉上,连串的迭代的话,应该是生成器快。因为生成器是惰性求值,不用像推导式经过中间变量容器的步骤。

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

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

    开题一句话,内容全靠编。
    一个细枝末节的小问题,不知道大家有什么看法?
    (问题上下文是 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
        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   ·   实用小工具   ·   2822 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 02:20 · PVG 10:20 · LAX 18:20 · JFK 21:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.