据我所知,有三种方法可以通过理解来创建生成器1。在
经典的:
def f1():
g = (i for i in range(10))
yield
变体:
yield from
变量(除了在函数内部引发SyntaxError
):
def f3():
g = [(yield from range(10))]
这三个变体导致不同的字节码,这并不奇怪。 第一个是最好的,这似乎是合乎逻辑的,因为它是一个专用的、直接的语法,可以通过理解来创建生成器。 然而,它并不是产生最短字节码的那个。在
在python3.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
变量是最快的(仍然与python3.6一起运行):
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
f3
的速度大约是f1
和{
正如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
结构有什么缺点?在
这都是相关的,所以我不想再提新的问题了,但是这个问题越来越奇怪了。
我试着比较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
。
这可能不会像你想象的那样。在
叫它:
^{pr2}$因为
yield from
不在理解范围内,它绑定到f2
函数,而不是隐式函数,从而将f2
转换为生成函数。在我记得有人指出它实际上不是迭代,但我不记得我在哪里看到的。当我重新发现这一点时,我正在自己测试代码。我没有在the mailing list post和{a2}中找到源代码。如果有人找到了来源,请告诉我或将其添加到帖子中,这样就可以记入贷方。在
你应该这样做:
这是一个生成器表达式。相当于
^{pr2}$但是如果您只想要一个包含
range(10)
元素的iterable,那么您可以这样做您不需要在函数中包装这些内容。在
如果你在这里学习写什么代码,你可以停止阅读。这篇文章的其余部分是一个冗长的技术性解释,解释了为什么其他代码片段被破坏了,不应该被使用,包括解释为什么你的计时也被破坏了。在
这个:
是一个破碎的建筑,应该在几年前被拆除。8年后的问题是originally reported,消除它的过程是finally beginning。别这么做。在
在python3上,虽然它仍然是语言,但它相当于
列表理解应该返回列表,但是由于
yield
,这个没有返回列表。它的行为有点像生成器表达式,它产生的结果与第一个代码片段相同,但是它构建了一个不必要的列表,并将其附加到末尾出现的StopIteration
。在这是令人困惑和浪费记忆。别这么做。(如果您想知道这些
None
的来源,请阅读PEP 342。)在Python2上,
g = [(yield i) for i in range(10)]
做了完全不同的事情。Python2没有给列表理解它们自己的作用域——特别是列表理解,而不是dict或set理解——因此yield
由包含这行的任何函数执行。在Python 2上,这:相当于
在pre-async sense中,使
f
成为基于生成器的协同程序。同样,如果你的目标是得到一个发电机,你浪费了大量的时间建立一个没有意义的清单。在这个:
太傻了,但这次没人怪Python。在
这里根本就没有理解力或genexp。括号不是一个列表理解;所有的工作都是由
yield from
完成的,然后构建一个包含yield from
(无用)返回值的1元素列表。您的f3
:当去掉不必要的列表构建时,简化为
或者,忽略所有协程支持功能
yield from
所做的你的时间安排也被打破了。在
在第一次计时中,}创建可以在这些函数中使用的生成器对象,尽管^{的生成器很奇怪。
f1
和{f3
没有这样做;f3
是一个生成函数。f3
的主体不在您的时间内运行,如果它运行,它的g
的行为将与其他函数的g
完全不同在您的第二次计时中,
f2
实际上没有运行,原因是f3
在前一次计时中被破坏了。在您的第二次计时中,f2
不迭代生成器。相反,yield from
将f2
转换为生成器函数本身。在此构造累积通过其
send()
方法传递回生成器的数据,并在迭代用尽时通过StopIteration
异常返回数据简单的生成器理解不会发生这样的事情:
^{pr2}$至于
yield from
版本-在Python 3.5中(我正在使用),它不能在函数之外工作,因此说明有点不同:好的,
send()
不适用于生成器yield
ingfrom
range()
,但至少让我们看看迭代结束时是什么:1请注意,即使您不使用
send()
方法,send(None)
也是假定的,因此以这种方式构造的生成器总是使用比普通生成器理解更多的内存(因为它必须将yield
表达式的结果累积到迭代结束):更新
关于三种变型之间的性能差异。}的两个主要原因之一)。然而,在这个特定的例子中}相同。在
yield from
优于其他两个,因为它消除了一个间接的层次(据我所知,这是引入{yield from
本身是多余的-g = [(yield from range(10))]
实际上几乎与{相关问题 更多 >
编程相关推荐