据我所知,通过推导式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
变体
>>> 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 倍。
注意当可迭代不是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
。
最佳答案
这是你应该做的:
g = (i for i in range(10))
这是一个生成器表达式。相当于
def temp(outer):
for i in outer:
yield i
g = temp(range(10))
但是如果你只是想要一个包含 range(10)
元素的迭代器,你可以这样做
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 不给列表推导式它们自己的范围——特别是列表推导式,而不是字典或集合推导式——所以 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
成为基于生成器的协程,在 pre-async sense 中.同样,如果您的目标是获得一个生成器,那么您已经浪费了大量时间来构建一个毫无意义的列表。
这个:
g = [(yield from range(10))]
很愚蠢,但这次没有任何责任归咎于 Python。
这里根本没有理解或 genexp。括号不是列表理解;所有工作都由 yield from
完成,然后您构建一个 1 元素列表,其中包含 yield from
的(无用)返回值。你的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
本身变成了生成器函数。
关于python - 生成器理解表达式之间的差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45190729/