据我所知,有三种方法可以通过理解创建生成器1。
经典之作:
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
变量>>> 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
比较显示,yield from
变量是最快的(仍然使用python 3.6运行):>>> 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倍。注:当iterable不是
range(10)
而是静态iterable时,结果或多或少相同,例如[0, 1, 2, 3, 4, 5]
。因此,速度差与
range
被某种方式优化无关。那么,这三种方法有什么区别呢?
更具体地说,
yield from
变量和其他变量之间有什么区别?这种正常的行为是不是自然构造比复杂构造慢?
从现在开始,我应该在所有脚本中用后者替换前者,还是使用
(elt for elt in it)
构造有任何缺点?编辑
这都是相关的,所以我不想打开一个新的问题,但这越来越陌生了。
我试着比较了
[(yield from it)]
和yield from
。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
所以。现在,迭代
range(10)
的速度是迭代裸机的186倍吗?您如何解释为什么迭代
[(yield from range(10))]
比迭代[(yield from range(10))]
快得多?1:对于持怀疑态度的人,下面的三个表达式确实产生了一个
range(10)
对象;尝试调用它们。 最佳答案
这就是你应该做的:
g = (i for i in range(10))
它是一个生成器表达式。它相当于
def temp(outer):
for i in outer:
yield i
g = temp(range(10))
但是,如果您只想要一个具有
range(10)
元素的iterable,那么您可以这样做。g = range(10)
您不需要在函数中包装这些内容。
如果你在这里学习写什么代码,你可以停止阅读。本文的其余部分是对其他代码片段为何被破坏而不应使用的一个冗长的技术解释,包括对为什么您的计时也被破坏的解释。
这是:
g = [(yield i) for i in range(10)]
是一个破碎的建筑,应该在几年前被拆除。问题发生8年后,移除它的过程是。别这样。
虽然它仍在语言中,但在python 3上,它相当于
def temp(outer):
l = []
for i in outer:
l.append((yield i))
return l
g = temp(range(10))
列表理解应该返回列表,但是由于
yield
的原因,这个列表没有返回。它的作用类似于生成器表达式,它产生的内容与第一个代码片段相同,但它构建了一个不必要的列表,并将其附加到末尾引发的StopIteration
中。>>> g = [(yield i) for i in range(10)]
>>> [next(g) for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None, None, None, None, None, None, None, None, None, None]
这是一种混乱和浪费的记忆。别这样。(如果您想知道所有这些
None
s的来源,请阅读originally reported)。在python 2上,
g = [(yield i) for i in range(10)]
做了完全不同的事情。python 2不提供列表理解它们自己的范围——具体来说是列表理解,而不是dict或set理解——因此,yield
由包含此行的任何函数执行。在python 2上,这是:def f():
g = [(yield i) for i in range(10)]
等于
def f():
temp = []
for i in range(10):
temp.append((yield i))
g = temp
使
f
成为基于生成器的协程,在finally beginning中。同样,如果您的目标是获得一个生成器,那么您已经浪费了大量时间来构建一个无意义的列表。这是:
g = [(yield from range(10))]
是愚蠢的,但这次没有责任在蟒蛇身上。
这里根本没有理解或genexp。括号不是列表理解;所有工作都是由
yield from
完成的,然后构建一个包含yield from
的(无用)返回值的1元素列表。您的def f3():
g = [(yield from range(10))]
当取消不必要的列表构建时,简化为
def f3():
yield from range(10)
或者,忽略所有协同工作支持的工作,
def f3():
for i in range(10):
yield i
你的时间也被打破了。
在第一次计时时,
f3
和yield from
创建可以在这些函数中使用的生成器对象,尽管f1
的生成器很奇怪。f2
不这样做;f2
是一个生成器函数。f3
的body不在计时中运行,如果在计时中运行,则其f3
的行为将与其他函数的行为非常不同。f3
和g
的计时实际上相当。def f4():
g = f3()
在第二个计时中,
g
实际上不运行,原因与前一个计时中的f1
中断的原因相同。在第二个计时中,f2
不是在生成器上迭代。相反,f2
将f3
转换为生成器函数本身。