为什么一台内置“yield”的发电机比另一台更快?

问题描述 投票:0回答:3

我有两个返回生成器的函数:

def f1():
    return (i for i in range(1000))

def f2():
    return ((yield i) for i in range(1000))

显然,从

f2()
返回的生成器比
f1()
慢两倍:

Python 3.6.5 (default, Apr  1 2018, 05:46:30) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import timeit, dis
>>> timeit.timeit("list(f1())", globals=globals(), number=1000)
0.057948426001530606
>>> timeit.timeit("list(f2())", globals=globals(), number=1000)
0.09769760200288147

我尝试使用 dis 来查看发生了什么,但无济于事:

>>> dis.dis(f1)
  2           0 LOAD_CONST               1 (<code object <genexpr> at 0x7ffff7ec6d20, file "<stdin>", line 2>)
              2 LOAD_CONST               2 ('f1.<locals>.<genexpr>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (1000)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE
>>> dis.dis(f2)
  2           0 LOAD_CONST               1 (<code object <genexpr> at 0x7ffff67a25d0, file "<stdin>", line 2>)
              2 LOAD_CONST               2 ('f2.<locals>.<genexpr>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (1000)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE

显然,

dis
的结果是一样的。

那么为什么从

f1()
返回的生成器比从
f2()
返回的生成器更快呢?调试这个的正确方法是什么?显然
dis
在这种情况下失败了。

编辑1:

及时使用

next()
代替
list()
会反转结果(或者在某些情况下它们是相同的):

>>> timeit.timeit("next(f1())", globals=globals(), number=10**6)
1.0030477920008707
>>> timeit.timeit("next(f2())", globals=globals(), number=10**6)
0.9416838550023385

编辑2:

显然这是 Python 中的错误,已在 3.8 中修复。请参阅 列表推导式和生成器表达式中的yield

内部有yield的生成器实际上会产生两个值。

python python-3.x performance generator bytecode
3个回答
4
投票

生成器表达式中的 Yield 实际上是一个错误,如相关问题中所述。

如果你想真正了解 dis 发生了什么,你需要内省代码对象的

co_const[0]
,所以:

>>> dis.dis(f1.__code__.co_consts[1])
  2           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE
>>> dis.dis(f2.__code__.co_consts[1])
  2           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 YIELD_VALUE
             14 POP_TOP
             15 JUMP_ABSOLUTE            3
        >>   18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

所以,它的产量是两倍。


2
投票

也许是因为

f2
返回的生成器返回两倍的元素数量。

看看会发生什么:

>>> def f2():
    return ((yield i) for i in range(10))
>>> g = f2()
>>> print([i for i in g])
[0, None, 1, None, 2, None, 3, None, 4, None, 5, None, 6, None, 7, None, 8, None, 9, None]

在生成器中使用

yield
会在每个实际项目后返回一个
None
元素。


0
投票

你对

f2()
的表述是第一个让我感到“不寻常”的事情。以大多数人的方式写作f2()会产生(双关语)非常不同的结果:

import timeit def f1(): return (i for i in range(1000)) def f2(): for i in range(1000): yield i res1 = timeit.timeit("list(f1())", globals=globals(), number=1000) print(res1) # 0.05318646361085916 res2 = timeit.timeit("list(f2())", globals=globals(), number=1000) print(res2) # 0.05284952304875785

所以两者似乎同样快。

正如其他人在答案中所说,这可能与您的

f2()

返回两倍的元素有关。

    

© www.soinside.com 2019 - 2024. All rights reserved.