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

2024-04-20 16:19:11 发布

您现在位置:Python中文网/ 问答频道 /正文

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

经典的:

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

yield变体:

^{pr2}$

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变量是最快的(仍然与python3.6一起运行):

>>> timeit(f1)
0.5334039637357152

>>> timeit(f2)
0.5358906506760719

>>> timeit(f3)
0.19329123352712596

f3的速度大约是f1和{}的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)而是静态iterable时,结果大致相同,例如[0, 1, 2, 3, 4, 5]。 因此,速度的差异与range的优化无关。在


那么,这三种方法有什么区别呢? 更具体地说,yield from变量与其他两个变量有什么区别?在

自然构造(elt for elt in it)比棘手的[(yield from it)]慢,这是正常的行为吗? 从现在开始,在我所有的脚本中,我是要用后者取代前者呢,还是使用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


Tags: infromfordefloadrangefunctionf2
3条回答

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

def f2():
    for i in [(yield from range(10))]:
        print(i)

叫它:

^{pr2}$

因为yield from不在理解范围内,它绑定到f2函数,而不是隐式函数,从而将f2转换为生成函数。在


我记得有人指出它实际上不是迭代,但我不记得我在哪里看到的。当我重新发现这一点时,我正在自己测试代码。我没有在the mailing list post和{a2}中找到源代码。如果有人找到了来源,请告诉我或将其添加到帖子中,这样就可以记入贷方。在

你应该这样做:

g = (i for i in range(10))

这是一个生成器表达式。相当于

^{pr2}$

但是如果您只想要一个包含range(10)元素的iterable,那么您可以这样做

g = range(10)

您不需要在函数中包装这些内容。在

如果你在这里学习写什么代码,你可以停止阅读。这篇文章的其余部分是一个冗长的技术性解释,解释了为什么其他代码片段被破坏了,不应该被使用,包括解释为什么你的计时也被破坏了。在


这个:

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

是一个破碎的建筑,应该在几年前被拆除。8年后的问题是originally reported,消除它的过程是finally beginning。别这么做。在

在python3上,虽然它仍然是语言,但它相当于

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。)

在Python2上,g = [(yield i) for i in range(10)]做了完全不同的事情。Python2没有给列表理解它们自己的作用域——特别是列表理解,而不是dict或set理解——因此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。在

这里根本就没有理解力或genexp。括号不是一个列表理解;所有的工作都是由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

你的时间安排也被打破了。在

在第一次计时中,f1和{}创建可以在这些函数中使用的生成器对象,尽管^{的生成器很奇怪。f3没有这样做;f3是一个生成函数。f3的主体不在您的时间内运行,如果它运行,它的g的行为将与其他函数的g完全不同

def f4():
    g = f3()

在您的第二次计时中,f2实际上没有运行,原因是f3在前一次计时中被破坏了。在您的第二次计时中,f2不迭代生成器。相反,yield fromf2转换为生成器函数本身。在

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

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

>>> 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]
>>> #          ^^^^^^^^^^^^^^^^^

简单的生成器理解不会发生这样的事情:

^{pr2}$

至于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()不适用于生成器yieldingfromrange(),但至少让我们看看迭代结束时是什么:

>>> 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本身是多余的-g = [(yield from range(10))]实际上几乎与{}相同。在

相关问题 更多 >