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

时间:2017-07-19 12:32:30

标签: python generator generator-expression

据我所知,有三种通过理解 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)而是静态可迭代时,结果或多或少相同,例如[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

3 个答案:

答案 0 :(得分:4)

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

此构造累积可以通过其send()方法传递回生成器的数据,并在迭代耗尽时通过StopIteration异常返回 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()不适用于生成器yield 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)完全相同。

答案 1 :(得分:3)

这就是你应该做的事情:

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年后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没有给出列表推导它们自己的范围 - 特别是列表推导,而不是dict或set comprehensions - 所以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成为基于生成器的协程。再说一遍,如果你的目标是获得一台发电机,你就浪费了很多时间来建立一个无意义的列表。

此:

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

很愚蠢,但这次没有任何责任归咎于Python。

这里根本没有理解或基因。括号不是列表理解;所有工作都由yield from完成,然后构建一个包含(无用)返回值yield from的1元素列表。您的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

你的时间也被打破了。

在你的第一个时间,f1f2创建可以在这些函数中使用的生成器对象,尽管f2的生成器很奇怪。 f3不这样做; f3 生成器函数。 f3的身体在你的计时中没有运行,如果确实如此,它的g的行为与其他函数'g完全不同。实际上与f1f2相当的时间将是

def f4():
    g = f3()

在您的第二个时间点,f2实际上并未运行,原因相同,f3在之前的时间被打破。在第二个时间,f2不会迭代生成器。相反,yield fromf2转换为生成器函数本身。

答案 2 :(得分:1)

这可能不符合你的想法。

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 postbug tracker thread搜索的来源。如果有人找到了来源,请告诉我或将其添加到帖子本身,这样可以记入。