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

Differences between generator comprehension expressions

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

三种变体导致不同的字节码,这并不奇怪。 第一个是最好的似乎是合乎逻辑的,因为它是一种专用的、直接的语法,可以通过理解创建生成器。 但是,它不是生成最短字节码的那个。

在Python3.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 倍

注意:当可迭代对象不是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 = [(yield i) for i in range(10)]

此构造累积数据,is/may 通过其 send() 方法传回生成器,并在迭代耗尽时通过 StopIteration 异常 returns 传回生成器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() 不适用于生成器 yielding 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).

这可能与您认为的不同。

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 nor the bug tracker thread。如果有人找到出处,请告诉我或添加到post本身,以便记入。

这是你应该做的:

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)

您不需要将其中的任何内容包装在函数中。

如果你是来学习写什么代码的,你可以停止阅读了。 post 的其余部分是一个很长的技术解释,解释为什么其他代码片段被破坏并且不应该被使用,包括解释为什么你的计时也被破坏。


这个:

g = [(yield i) for i in range(10)]

是一个损坏的构造,应该在几年前就被移除了。 8 年后的问题是 originally reported, the process to remove it is finally beginning。别这样。

虽然它还在语言中,但在 Python 3 上,它等同于

def temp(outer):
    l = []
    for i in outer:
        l.append((yield i))
    return l
g = temp(range(10))

列表理解应该是 return 列表,但由于 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

pre-async sense 中制作 f 一个 generator-based 协程。同样,如果您的目标是获得一个生成器,那么您已经浪费了很多时间来构建一个毫无意义的列表。


这个:

g = [(yield from range(10))]

傻了,不过none这回怪在Python身上了。

这里根本没有理解或 genexp。括号不是列表理解;所有工作都由 yield from 完成,然后您构建一个 1 元素列表,其中包含 yield from 的(无用)return 值。你的 f3:

def f3():
    g = [(yield from range(10))]

去除不必要的 list-building 后,简化为

def f3():
    yield from range(10)

或者,忽略所有协程支持的东西 yield from

def f3():
    for i in range(10):
        yield i

你的时间也坏了。

在您的第一次计时中,f1f2 创建了可以在这些函数内部使用的生成器对象,尽管 f2 的生成器很奇怪。 f3 不会那样做; f3 一个生成器函数。 f3's body does not run in your timings, and if it did, its g would behave quite unlike the other functions'g秒。实际上与 f1f2 相当的时间是

def f4():
    g = f3()

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