其中一种是经典的写法:
def f1():
g = (i for i in range(10))
yield
变体:
def f2():
g = [(yield i) for i in range(10)]
使用yield from
的变量(在函数外部引用会触发SyntaxError
错误):
def f3():
g = [(yield from range(10))]
三种变体会产生不同的字节码,这并不奇怪。似乎第一个是最好的选择,因为它是通过推导式创建生成器的专用,简单的语法。然而,它并不能产生最短的字节码。
在Python 3.6中反汇编
经典的生成器推导式
>>> dis.dis(f1)
4 0 LOAD_CONST 1 (<code object <genexpr> at...>)
2 LOAD_CONST 2 ('f1.<locals>.<genexpr>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
5 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield
变体
>>> dis.dis(f2)
8 0 LOAD_CONST 1 (<code object <listcomp> at...>)
2 LOAD_CONST 2 ('f2.<locals>.<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
9 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield from
variant
>>> dis.dis(f3)
12 0 LOAD_GLOBAL 0 (range)
2 LOAD_CONST 1 (10)
4 CALL_FUNCTION 1
6 GET_YIELD_FROM_ITER
8 LOAD_CONST 0 (None)
10 YIELD_FROM
12 BUILD_LIST 1
14 STORE_FAST 0 (g)
13 16 LOAD_FAST 0 (g)
18 RETURN_VALUE
此外,timeit
对比表明,在 Python 3.6 中 yield from
的变体是最快的:
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
f3
的速度比 f1
和 f2
快大约 2.7 倍。
正如Leon在评论中提到的,生成器的效率最好通过它能被迭代的速度来衡量。 因此,我改变了这三个函数,使它们迭代生成器,并调用一个虚拟函数。
def f():
pass
def fn():
g = ...
for _ in g:
f()
结果更加明显:
>>> timeit(f1)
1.6017412817975778
>>> timeit(f2)
1.778684261368946
>>> timeit(f3)
0.1960603619517669
f3
现在比f1
快8.4倍,比f2
快9.3倍。
注意:当可迭代对象不是range(10)
而是静态的可迭代对象时,例如[0, 1, 2, 3, 4, 5]
,结果或多或少相同。
因此,速度差异与range
被优化无关。
那么,这三种方法有什么区别?
更具体地说,yield from
变体和另外两个有什么区别?
自然构造(elt for elt in it)
比诡计多端的[(yield from it)]
慢是正常行为吗?
从现在开始,我是否应该在我的所有脚本中用后者替换前者,或者使用yield from
结构存在任何缺点?
编辑
这一切都是相关的,所以我不想提一个新问题,但这变得更加奇怪了。
我尝试比较range(10)
和[(yield from range(10))]
。
def f1():
for i in range(10):
print(i)
def f2():
for i in [(yield from range(10))]:
print(i)
>>> timeit(f1, number=100000)
26.715589237537195
>>> timeit(f2, number=100000)
0.019948781941049987
那么,现在用[(yield from range(10))]
循环迭代的速度比裸的range(10)
快186倍?
你如何解释为什么用[(yield from range(10))]
循环迭代比用range(10)
快这么多?
1: 对于怀疑者,接下来的三个表达式确实会生成一个generator
对象;尝试调用type
。
<genexpr>
而第二个加载了一个<listcomp>
。 - Right leg