Python:为什么一个生成器比另一个内部有 `yield` 的生成器快?
Python: why is one generator faster than other with `yield` inside?
我有两个函数返回生成器:
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 的生成器实际上会产生两个值。
可能是因为f2
返回的生成器returns元素数量的两倍
看看会发生什么:
>>> 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
returns 每个实际项目后的 None
元素。
你对 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()
returns 元素数量是原来的两倍有关。
生成器表达式中的 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
所以,它产生了两次。
我有两个函数返回生成器:
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 的生成器实际上会产生两个值。
可能是因为f2
返回的生成器returns元素数量的两倍
看看会发生什么:
>>> 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
returns 每个实际项目后的 None
元素。
你对 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()
returns 元素数量是原来的两倍有关。
生成器表达式中的 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
所以,它产生了两次。