【问题标题】:Differences between generator comprehension expressions生成器理解表达式之间的差异
【发布时间】:2017-12-24 17:38:46
【问题描述】:

据我所知,通过推导式创建生成器的方法有三种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 倍。

注意:当可迭代对象不是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

【问题讨论】:

  • “最佳表达”“最佳表达”如何定义?
  • 第一个和第二个字节码其实是一样的(除了返回对象的名字)等等
  • @WillemVanOnsem 不完全是,第一个加载一个&lt;genexpr&gt;,而第二个加载一个&lt;listcomp&gt;
  • @Rightleg this 是我的想法。
  • @Chris_Rands 这个话题真的让我很困惑。为什么一个专门的结构比一个有点做作和反直觉的结构慢?

标签: python generator generator-expression


【解决方案1】:

这是你应该做的:

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)

您不需要将任何这些包装在函数中。

如果您是来学习编写什么代码的,您可以停止阅读。这篇文章的其余部分是关于为什么其他代码 sn-ps 被破坏并且不应该使用的冗长技术解释,包括解释为什么你的时间也被破坏。


这个:

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,这个没有。它的行为有点像生成器表达式,它产生的结果与您的第一个 sn-p 相同,但它构建了一个不必要的列表并将其附加到最后引发的 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 来自哪里,请阅读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 成为基于生成器的协程,在pre-async sense 中。同样,如果您的目标是获得一个生成器,那么您已经浪费了大量时间来构建一个毫无意义的列表。


这个:

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

很傻,但这次不怪 Python。

这里根本没有理解或基因表达。括号不是列表理解;所有的工作都由yield from 完成,然后您构建一个包含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 的行为将与其他函数的 gs 完全不同。实际上与f1f2 相当的时间是

def f4():
    g = f3()

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

【讨论】:

    【解决方案2】:

    这可能不会像你想象的那样。

    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 搜索的源代码。如果有人找到了出处,请告诉我或将其添加到帖子本身,以便注明出处。

    【讨论】:

    • 我刚刚从我的答案中删除了一个后续代码奇怪。结果发现有我不知道的 for 循环的特殊 REPL 处理,因为我通常使用 IPython 的 REPL。奇怪与生成器无关。
    【解决方案3】:
    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() 不适用于生成器 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) 几乎相同。

    【讨论】:

    • yield from 版本适用于函数内部
    • @Chris_Rands 谢谢。更新了答案。
    • 我不确定要得出什么结论。 yield from 变体累积 None 但只有一次,不是吗?如果是这样,我认为使用该构造没有任何缺点......
    猜你喜欢
    • 2021-07-27
    • 2010-09-21
    • 2023-02-02
    • 2021-12-08
    • 1970-01-01
    • 2013-04-12
    • 2013-09-10
    • 1970-01-01
    • 2020-08-09
    相关资源
    最近更新 更多