据我所知,有三种方法可以通过理解创建生成器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的速度是f1f2的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]

这是一种混乱和浪费的记忆。别这样。(如果您想知道所有这些Nones的来源,请阅读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

你的时间也被打破了。
在第一次计时时,f3yield from创建可以在这些函数中使用的生成器对象,尽管f1的生成器很奇怪。f2不这样做;f2是一个生成器函数。f3的body不在计时中运行,如果在计时中运行,则其f3的行为将与其他函数的行为非常不同。f3g的计时实际上相当。
def f4():
    g = f3()

在第二个计时中,g实际上不运行,原因与前一个计时中的f1中断的原因相同。在第二个计时中,f2不是在生成器上迭代。相反,f2f3转换为生成器函数本身。

10-08 04:36