簡體   English   中英

生成器理解表達式之間的差異

[英]Differences between generator comprehension expressions

據我所知,有三種通過理解創建生成器的方法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變體的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倍。

正如萊昂在評論中提到的那樣,發電機的效率最好用它可以迭代的速度來衡量。 所以我更改了三個函數,以便迭代生成器,並調用虛函數。

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

 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'

OK, 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]

UPDATE

關於三種變體之間的性能差異。 yield from的其他兩拍,因為它消除間接(其中,盡我的理解,是兩個主要的原因之一的水平yield from引入)。 然而,在這個特定的例子中yield from自身的yield from是多余的 - g = [(yield from range(10))]實際上幾乎與g = range(10)

這是你應該做的:

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 ,這個沒有。 它有點像生成器表達式,它產生與第一個片段相同的東西,但它構建了一個不必要的列表並將其附加到最后引發的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 s是,讀來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

使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 from f2變為生成器函數本身的yield from

這可能不符合您的想法。

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轉換為生成函數。


我記得看到有人指出它實際上並沒有迭代,但我不記得我在哪里看到它。 當我重新發現這個時,我正在測試代碼。 我沒有找到源搜索郵件列表帖子bug跟蹤器線程 如果有人找到了來源,請告訴我或將其添加到帖子本身,這樣可以記入。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM