16

据我所知,通过理解1创建生成器的方法有三种。

经典之一:

def f1():
    g = (i for i in range(10))

变体yield

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

变体(在函数内部yield from引发SyntaxErrorexcept ):

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

f3f1大约是和的 2.7 倍f2

正如Leon在评论中提到的,发电机的效率最好通过它可以迭代的速度来衡量。所以我改变了三个函数,让它们遍历生成器,并调用一个虚拟函数。

def f():
    pass

def fn():
    g = ...
    for _ in g:
        f()

结果更加明显:

>>> timeit(f1)
1.6017412817975778

>>> timeit(f2)
1.778684261368946

>>> timeit(f3)
0.1960603619517669

f3现在是 8.4 倍,是f19.3 倍f2

注意:当可迭代对象不是静态可迭代对象时,结果或多或少是相同的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))]速度是迭代裸机的 186 倍range(10)

你如何解释为什么迭代[(yield from range(10))]比迭代快得多range(10)


1:对于持怀疑态度的人,以下三个表达确实产生了一个generator对象;试着打电话type给他们。

4

3 回答 3

4

这是你应该做的:

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 年后,消除它的过程终于开始了。不要这样做。

虽然它仍在语言中,但在 Python 3 上,它相当于

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

列表推导应该返回列表,但由于yield,这个没有。它有点像生成器表达式,它产生与你的第一个片段相同的东西,但它构建了一个不必要的列表并将其附加到最后的StopIterationraise 上。

>>> 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 来自哪里,请阅读PEP 342。)

在 Python 2 上,g = [(yield i) for i in range(10)]做一些完全不同的事情。Python 2 没有给列表推导他们自己的范围——特别是列表推导,而不是 dict 或集合推导——所以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基于生成器的协程。同样,如果您的目标是获得一个生成器,那么您已经浪费了大量时间来构建一个毫无意义的列表。


这个:

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

很傻,但这次不怪 Python。

这里根本没有理解或基因表达。括号不是列表理解;所有工作都由 完成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

你的时间也被打破了。

在您的第一次时间,f1f2创建可以在这些函数中使用的生成器对象,尽管f2' 的生成器很奇怪。f3不这样做;f3 一个生成器函数。f3的主体不会按照您的时间运行,如果它运行了,它的g行为将与其他函数完全不同g。一个实际上可以与f1并且f2将是可比较的时间

def f4():
    g = f3()

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

于 2018-04-03T17:26:59.750 回答
4
g = [(yield i) for i in range(10)]

此构造累积通过其方法传递回生成器的数据,并在迭代结束时通过异常send()返回它1StopIteration

>>> 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()不适用于生成器yieldfrom 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).

于 2017-07-19T12:48:40.873 回答
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变成生成器函数。


我记得看到有人指出它实际上并没有迭代,但我不记得我在哪里看到的。当我重新发现这一点时,我正在自己测试代码。我没有找到通过邮件列表帖子错误跟踪线程搜索的来源。如果有人找到来源,请告诉我或将其添加到帖子本身,以便记入贷方。

于 2017-12-14T05:10:41.743 回答