Rig*_*leg 16 python generator generator-expression
据我所知,有三种通过理解创建生成器的方法1.
经典之一:
def f1():
g = (i for i in range(10))
Run Code Online (Sandbox Code Playgroud)
该yield
变种:
def f2():
g = [(yield i) for i in range(10)]
Run Code Online (Sandbox Code Playgroud)
的yield from
变体(即提出了SyntaxError
除了一个函数的内部):
def f3():
g = [(yield from range(10))]
Run Code Online (Sandbox Code Playgroud)
这三种变体导致不同的字节码,这并不奇怪.第一个是最好的,这似乎是合乎逻辑的,因为它是通过理解创建生成器的专用,直接的语法.但是,它不是产生最短字节码的那个.
在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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
此外,timeit
比较显示yield from
变体是最快的(仍然使用Python 3.6运行):
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
Run Code Online (Sandbox Code Playgroud)
f3
或多或少是f1
和的速度的2.7倍f2
.
正如莱昂在评论中提到的那样,发电机的效率最好用它可以迭代的速度来衡量.所以我更改了三个函数,以便迭代生成器,并调用虚函数.
def f():
pass
def fn():
g = ...
for _ in g:
f()
Run Code Online (Sandbox Code Playgroud)
结果更加明显:
>>> timeit(f1)
1.6017412817975778
>>> timeit(f2)
1.778684261368946
>>> timeit(f3)
0.1960603619517669
Run Code Online (Sandbox Code Playgroud)
f3
现在速度是原来的8.4倍,速度是f1
原来的9.3倍f2
.
注意:当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
Run Code Online (Sandbox Code Playgroud)
所以.现在,迭代的[(yield from range(10))]
速度是裸露迭代的186倍range(10)
?
你如何解释为什么迭代[(yield from range(10))]
比迭代更快range(10)
?
1:对于持怀疑态度,后面的三个表达式会产生一个generator
对象; 试着打电话type
给他们.
这就是你应该做的:
g = (i for i in range(10))
Run Code Online (Sandbox Code Playgroud)
这是一个生成器表达式。它相当于
def temp(outer):
for i in outer:
yield i
g = temp(range(10))
Run Code Online (Sandbox Code Playgroud)
但如果你只是想要一个包含 元素的迭代range(10)
,你可以这样做
g = range(10)
Run Code Online (Sandbox Code Playgroud)
您不需要将其中任何内容包装在函数中。
如果您来这里是为了了解要编写哪些代码,您可以停止阅读。这篇文章的其余部分是一个冗长的技术解释,说明为什么其他代码片段被破坏并且不应该使用,包括解释为什么你的计时也被破坏。
这:
g = [(yield i) for i in range(10)]
Run Code Online (Sandbox Code Playgroud)
这是一个破损的结构,几年前就应该被拆除。该问题最初被报告 8 年后,消除该问题的过程终于开始了。不要这样做。
虽然它仍在语言中,但在 Python 3 上,它相当于
def temp(outer):
l = []
for i in outer:
l.append((yield i))
return l
g = temp(range(10))
Run Code Online (Sandbox Code Playgroud)
列表推导式应该返回列表,但由于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]
Run Code Online (Sandbox Code Playgroud)
这很令人困惑并且浪费内存。不要这样做。(如果您想知道所有这些None
s 来自哪里,请阅读PEP 342。)
在 Python 2 上,g = [(yield i) for i in range(10)]
做了完全不同的事情。Python 2 没有为列表推导式提供自己的作用域 - 特别是列表推导式,而不是字典推导式或集合推导式 - 因此yield
由包含此行的任何函数执行。在 Python 2 上,这样:
def f():
g = [(yield i) for i in range(10)]
Run Code Online (Sandbox Code Playgroud)
相当于
def f():
temp = []
for i in range(10):
temp.append((yield i))
g = temp
Run Code Online (Sandbox Code Playgroud)
在预异步意义上制作f
基于生成器的协程。再说一遍,如果您的目标是获得一台生成器,那么您就浪费了大量时间来构建毫无意义的列表。
这:
g = [(yield from range(10))]
Run Code Online (Sandbox Code Playgroud)
很愚蠢,但这一次不能把责任归咎于 Python。
这里根本没有理解或 genexp。括号不是列表理解;所有工作均由 完成yield from
,然后构建一个包含 的(无用)返回值的 1 元素列表yield from
。你的f3
:
def f3():
g = [(yield from range(10))]
Run Code Online (Sandbox Code Playgroud)
当删除不必要的列表构建时,简化为
def f3():
yield from range(10)
Run Code Online (Sandbox Code Playgroud)
或者,忽略所有协程支持的内容yield from
,
def f3():
for i in range(10):
yield i
Run Code Online (Sandbox Code Playgroud)
你的时间安排也被打破了。
在您的第一次计时中,f1
创建f2
可在这些函数内部使用的生成器对象,尽管f2
的生成器很奇怪。f3
不这样做;f3
是一个生成器函数。f3
的主体不会按照您的计时运行,如果这样做,它的g
行为将与其他函数非常不同g
。f1
实际上可以与和f2
进行比较的时间安排
def f4():
g = f3()
Run Code Online (Sandbox Code Playgroud)
在你的第二个计时中,f2
实际上并没有运行,出于同样的原因f3
在前一个计时中被破坏了。在您的第二次计时中,f2
没有迭代生成器。相反,它们本身yield from
变成f2
了生成器函数。