生成器理解表达式之间的差异

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给他们.

use*_*ica 4

这就是你应该做的:

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)

这很令人困惑并且浪费内存。不要这样做。(如果您想知道所有这些Nones 来自哪里,请阅读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行为将与其他函数非常不同gf1实际上可以与和f2进行比较的时间安排

def f4():
    g = f3()
Run Code Online (Sandbox Code Playgroud)

在你的第二个计时中,f2实际上并没有运行,出于同样的原因f3在前一个计时中被破坏了。在您的第二次计时中,f2没有迭代生成器。相反,它们本身yield from变成f2了生成器函数。