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

Python 生成器理解表达式之间的差异,python,generator,generator-expression,Python,Generator,Generator Expression,据我所知,有三种方法可以通过理解1创建生成器 经典的一个: def f1(): g = (i for i in range(10)) 产量变量: def f2(): g = [(yield i) for i in range(10)] 来自变量的收益(除了函数内部之外,它会引发一个SyntaxError): 这三种变体导致不同的字节码,这并不奇怪。 第一个是最好的,这似乎是合乎逻辑的,因为它是通过理解创建生成器的专用、简单的语法。 然而,它并不是产生最短字节码的那个 在Pyt

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

经典的一个:

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

def f2():
    g = [(yield i) for i in range(10)]
来自变量的
收益(除了函数内部之外,它会引发一个
SyntaxError
):

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

在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
变体中获得收益

>>> 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
>>> 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
比较表明,
变体的
收益率是最快的(仍然使用Python 3.6运行):

f3
的速度大约是
f1
f2
的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不是
范围(10)
而是静态iterable,例如
[0,1,2,3,4,5]
,则结果大致相同。 因此,速度的差异与
范围的优化无关


那么,这三种方式的区别是什么? 更具体地说,来自
变量的
收益率与另外两个变量之间有什么区别

这是正常的行为吗?自然结构
(elt为elt中的elt)
比复杂的
[(从中产生)]
慢吗? 从现在起,我应该在所有脚本中用后者替换前者,还是使用
构造中产生的
有任何缺点


编辑 这一切都是相关的,所以我不想提出一个新问题,但这越来越奇怪了。 我试着比较
范围(10)
[(范围(10)的收益率)]

所以。现在,迭代
[(范围(10)的收益率]
的速度是迭代裸
范围(10)
的186倍

您如何解释为什么迭代
[(范围(10)的收益率]
比迭代
范围(10)
快得多


1:对于怀疑论者,下面的三个表达式确实生成了一个
生成器
对象;尝试调用
键入

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

好的,
send()
对生成器不起作用
yield
ing
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]
>>> #          ^^^^^^
>>> 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]

更新


关于三种变体之间的性能差异<代码>从
产生的收益优于其他两种,因为它消除了一定程度的间接性(据我所知,这是引入
产生收益的两个主要原因之一)。然而,在这个特定的例子中,
产生的收益本身是多余的-
g=[(从范围(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}
>>def f2():
...     对于i in[(范围(10)的收益率]:
...         印刷品(一)
...
>>>f2()#不打印。
>>>set(f2())#打印'None',因为`(范围(10)的产量)`计算结果为'None'。
没有一个
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
由于
产生的
收益不在理解范围内,因此它被绑定到
f2
函数而不是隐式函数,从而将
f2
转化为生成函数

def f():
    pass

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


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

这是您应该做的:

g = (i for i in range(10))
这是一个生成器表达式。相当于

def temp(outer):
    for i in outer:
        yield i
g = temp(range(10))
def temp(outer):
    l = []
    for i in outer:
        l.append((yield i))
    return l
g = temp(range(10))
def f():
    temp = []
    for i in range(10):
        temp.append((yield i))
    g = temp
但是,如果您只是想要一个包含
范围(10)
元素的iterable,您可以这样做

g = range(10)
您不需要将这些内容包装到函数中

def f():
    pass

def fn():
    g = ...
    for _ in g:
        f()
如果您在这里学习编写什么代码,您可以停止阅读。这篇文章的其余部分是一篇冗长的技术性解释,解释了为什么其他代码片段被破坏,不应该使用,包括解释了为什么计时也被破坏


这:

是一个多年前就应该拆除的破建筑。问题解决8年后,解决问题的过程是。不要这样做

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

def temp(outer):
    for i in outer:
        yield i
g = temp(range(10))
def temp(outer):
    l = []
    for i in outer:
        l.append((yield i))
    return l
g = temp(range(10))
def f():
    temp = []
    for i in range(10):
        temp.append((yield i))
    g = temp
列表理解应该返回列表,但由于
的收益率
,这个不能返回列表。它的行为有点像生成器表达式,它生成的内容与第一个代码片段相同,但它构建了一个不必要的列表,并将其附加到最后提出的
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]
相当于

def temp(outer):
    for i in outer:
        yield i
g = temp(range(10))
def temp(outer):
    l = []
    for i in outer:
        l.append((yield i))
    return l
g = temp(range(10))
def f():
    temp = []
    for i in range(10):
        temp.append((yield i))
    g = temp
在中创建基于生成器的协同程序。再说一次,如果你的目标是得到一个生成器,你就浪费了很多时间来建立一个毫无意义的列表


这:

这是愚蠢的,但这次没有任何责任在Python身上

这里根本没有理解力或基因。括号不是一个列表;所有的工作
def f():
    temp = []
    for i in range(10):
        temp.append((yield i))
    g = temp
g = [(yield from range(10))]
def f3():
    g = [(yield from range(10))]
def f3():
    yield from range(10)
def f3():
    for i in range(10):
        yield i
def f4():
    g = f3()