据我所知,有三种通过理解 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)
而是静态可迭代时,结果或多或少相同,例如[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
。
答案 0 :(得分:4)
g = [(yield i) for i in range(10)]
此构造累积可以通过其send()
方法传递回生成器的数据,并在迭代耗尽时通过StopIteration
异常返回 1 :
>>> g = [(yield i) for i in range(3)]
>>> next(g)
0
>>> g.send('abc')
1
>>> g.send(123)
2
>>> g.send(4.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: ['abc', 123, 4.5]
>>> # ^^^^^^^^^^^^^^^^^
普通的生成器理解不会发生这样的事情:
>>> g = (i for i in range(3))
>>> next(g)
0
>>> g.send('abc')
1
>>> g.send(123)
2
>>> g.send(4.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
至于yield from
版本 - 在Python 3.5(我正在使用)中,它不能在函数外部工作,所以插图有点不同:
>>> def f(): return [(yield from range(3))]
...
>>> g = f()
>>> next(g)
0
>>> g.send(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in f
AttributeError: 'range_iterator' object has no attribute 'send'
好的,send()
不适用于生成器yield
from
range()
,但让我们至少看看迭代结束时的内容:
>>> g = f()
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None]
>>> # ^^^^^^
1 请注意,即使您不使用send()
方法,也假设send(None)
,因此以这种方式构造的生成器总是比普通生成器使用更多内存理解(因为它必须累积yield
表达式的结果直到迭代结束):
>>> g = [(yield i) for i in range(3)]
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None, None, None]
<强>更新强>
关于三种变体之间的性能差异。 yield from
胜过其他两个,因为它消除了一定程度的间接(据我所知,这是yield from
引入的两个主要原因之一)。但是,在此特定示例中yield from
本身是多余的 - g = [(yield from range(10))]
实际上几乎与g = range(10)
完全相同。
答案 1 :(得分:3)
这就是你应该做的事情:
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年后originally reported,删除问题的过程为finally beginning。不要这样做。
虽然它仍在语言中,但在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
的来源,请阅读PEP 342。)
在Python 2上,g = [(yield i) for i in range(10)]
做了一些完全不同的事情。 Python 2没有给出列表推导它们自己的范围 - 特别是列表推导,而不是dict或set comprehensions - 所以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
在pre-async sense中使f
成为基于生成器的协程。再说一遍,如果你的目标是获得一台发电机,你就浪费了很多时间来建立一个无意义的列表。
此:
g = [(yield from range(10))]
很愚蠢,但这次没有任何责任归咎于Python。
这里根本没有理解或基因。括号不是列表理解;所有工作都由yield from
完成,然后构建一个包含(无用)返回值yield from
的1元素列表。您的f3
:
def f3():
g = [(yield from range(10))]
当剥离不必要的列表构建时,简化为
def f3():
yield from range(10)
或忽略yield from
所做的所有协程支持,
def f3():
for i in range(10):
yield i
你的时间也被打破了。
在你的第一个时间,f1
和f2
创建可以在这些函数中使用的生成器对象,尽管f2
的生成器很奇怪。 f3
不这样做; f3
是生成器函数。 f3
的身体在你的计时中没有运行,如果确实如此,它的g
的行为与其他函数'g
完全不同。实际上与f1
和f2
相当的时间将是
def f4():
g = f3()
在您的第二个时间点,f2
实际上并未运行,原因相同,f3
在之前的时间被打破。在第二个时间,f2
不会迭代生成器。相反,yield from
将f2
转换为生成器函数本身。
答案 2 :(得分:1)
这可能不符合你的想法。
def f2():
for i in [(yield from range(10))]:
print(i)
称之为:
>>> def f2():
... for i in [(yield from range(10))]:
... print(i)
...
>>> f2() #Doesn't print.
<generator object f2 at 0x02C0DF00>
>>> set(f2()) #Prints `None`, because `(yield from range(10))` evaluates to `None`.
None
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
因为yield from
不在理解范围内,所以它被绑定到f2
函数而不是隐式函数,将f2
转换为生成函数。
我记得看到有人指出它实际上没有迭代,但我无法记住我在哪里看到它。当我重新发现这个时,我正在测试代码。我没有找到通过the mailing list post和bug tracker thread搜索的来源。如果有人找到了来源,请告诉我或将其添加到帖子本身,这样可以记入。