python - yield用途 - “yield”關鍵字有什麼作用?




yield python3 (20)

yield關鍵字在Python中有什麼作用?

回答大綱/摘要

  • 帶有yield的函數在被調用時返回一個Generator
  • 生成器是迭代器,因為它們實現了迭代器協議 ,因此您可以迭代它們。
  • 還可以生成器發送信息 ,使其在概念上成為協程
  • 在Python 3中,您可以在兩個方向上從一個生成器委派給另一個生成器,其中yield from
  • (附錄批評了幾個答案,包括最重要的答案,並討論了在發電機中使用return 。)

發電機:

yield在函數定義中只是合法的,並且函數定義中包含yield使它返回一個生成器。

生成器的想法來自其他語言(見腳註1),具有不同的實現。 在Python的Generators中,代碼的執行在yield的時候被frozen 。 當調用生成器時(下面討論方法),執行將恢復,然後在下一次生成時凍結。

yield提供了一種實現迭代器協議的簡單方法, 該協議由以下兩種方法定義: __iter__next (Python 2)或__next__ (Python 3)。 這兩種方法都使對象成為迭代器,您可以使用collections模塊中的Iterator Abstract Base Class進行類型檢查。

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

生成器類型是迭代器的子類型:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

如有必要,我們可以像這樣打字檢查:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Iterator一個特性是,一旦耗盡 ,您就無法重用或重置它:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

如果您想再次使用其功能,則必須製作另一個(參見腳註2):

>>> list(func())
['I am', 'a generator!']

可以以編程方式生成數據,例如:

def func(an_iterable):
    for item in an_iterable:
        yield item

上面的簡單生成器也等同於下面 - 從Python 3.3開始(在Python 2中不可用),你可以使用yield from

def func(an_iterable):
    yield from an_iterable

但是, yield from也允許委託給子發電機,這將在下面關於與子程序的協同授權的部分中解釋。

協同程序:

yield表示允許將數據發送到生成器的表達式(參見腳註3)

下面是一個示例,請注意received變量,該變量將指向發送到生成器的數據:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

首先,我們必須使用內置函數排隊生成器, next 。 它將調用適當的next__next__方法,具體取決於您使用的Python版本:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

現在我們可以將數據發送到生成器。 ( 發送None與調用next相同 。):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

合作代表團參與Sub-Coroutine的yield from

現在,回想一下,Python 3中提供了yield from 。這允許我們將協同程序委託給子協會:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

現在我們可以將功能委託給子生成器,它可以像生成器一樣使用,如上所述:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

您可以在PEP 380中閱讀有關yield from的精確語義的更多信息

其他方法:關閉並拋出

close方法在函數執行被凍結時引發GeneratorExit 。 這也將由__del__調用,因此您可以將任何清理代碼放在處理GeneratorExit

>>> my_account.close()

您還可以拋出一個異常,該異常可以在生成器中處理或傳播回用戶:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

結論

我相信我已經涵蓋了以下問題的所有方面:

yield關鍵字在Python中有什麼作用?

事實證明, yield很高。 我相信我可以為此添加更全面的例子。 如果您想要更多或有一些建設性的批評,請通過下面的評論告訴我。

附錄:

批評最高/已接受的答案**

  • 關於什麼使迭代成為混亂,僅使用列表作為示例。 請參閱上面的參考資料,但總結一下:iterable有一個__iter__方法返回迭代器迭代器提供了一個.next (Python 2或.__next__ (Python 3)方法,它被for循環隱式調用,直到它引發StopIteration ,一旦它啟動,它將繼續這樣做。
  • 然後它使用生成器表達式來描述生成器是什麼。 由於生成器只是創建迭代器的一種方便方法,因此它只會混淆事情,我們仍然沒有達到yield部分。
  • 控制生成器耗盡時,他調用.next方法,而在next他應該使用內置函數。 這將是一個適當的間接層,因為他的代碼在Python 3中不起作用。
  • Itertools? 這與yield完全沒有關係。
  • 在Python 3中沒有討論yield提供的方法以及新功能。 最高/接受的答案是一個非常不完整的答案。

對答案的批判表明在生成器表達或理解中的yield

語法當前允許列表理解中的任何表達。

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

由於屈服是一種表達,因此在一些理解或生成器表達中使用它已被一些人吹捧為有趣 - 儘管沒有引用特別好的用例。

CPython核心開發人員正在討論棄用其配額 。 這是郵件列表中的相關帖子:

2017年1月30日19:05,Brett Cannon寫道:

在Sun,2017年1月29日16:39 Craig Rodrigues寫道:

無論採用哪種方法,我都行。 恕我直言,將它們放在Python 3中的方式並不好。

我的投票是它是一個SyntaxError,因為你沒有得到你對語法的期望。

我同意這對我們來說是一個明智的地方,因為任何依賴當前行為的代碼實在太聰明而無法維護。

在達到目標方面,我們可能會想:

  • 3.7中的SyntaxWarning或DeprecationWarning
  • 2.7.x中的Py3k警告
  • 3.8中的SyntaxError

乾杯,尼克。

- Nick Coghlan | ncoghlan at gmail.com | 澳大利亞布里斯班

此外,還有一個突出的問題(10544)似乎指向了這個永遠不是一個好主意的方向(PyPy,一個用Python編寫的Python實現,已經提出了語法警告。)

最後,直到CPython的開發人員告訴我們: 不要將yield放在生成器表達式或理解中。

生成器中的return語句

Python 2中

在生成器函數中,不允許return語句包含expression_list 。 在該上下文中,裸return表示生成器已完成並將導致StopIteration被引發。

An expression_list基本上是用逗號分隔的任意數量的表達式 - 實質上,在Python 2中,您可以使用停止生成器return,但不能返回值。

Python 3中

在生成器函數中,該return語句指示生成器已完成並將導致StopIteration生成。返回值(如果有)用作構造的參數StopIteration並成為StopIteration.value屬性。

腳註

  1. 提議中引用了CLU,Sather和Icon語言,以便將生成器的概念引入Python。一般的想法是函數可以維持內部狀態並根據用戶的要求產生中間數據點。這承諾在性能上優於其他方法,包括Python線程,在某些系統上甚至不可用。

  2. 這意味著,例如,xrange對象(range在Python 3中)不是Iterators,即使它們是可迭代的,因為它們可以被重用。與列表一樣,它們的__iter__方法返回迭代器對象。

  3. yield最初是作為語句引入的,這意味著它只能出現在代碼塊中一行的開頭。現在yield創建一個yield表達式。https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt這種變化提出以允許用戶將數據發送到發電機,就像一個會接受它。要發送數據,必須能夠將其分配給某些內容,為此,語句將無效。

https://code.i-harness.com

Python中yield關鍵字的用途是什麼? 它有什麼作用?

例如,我正在嘗試理解這段代碼1

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

這是來電者:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

調用方法_get_child_candidates時會發生什麼? 列表是否返回? 單個元素? 它又被召喚了嗎? 後續通話何時停止?

1.代碼來自Jochen Schulz(jrschulz),他為度量空間創建了一個很棒的Python庫。 這是完整源代碼的鏈接: 模塊mspace


Grokking yield捷徑

當你看到一個帶有yield語句的函數時,應用這個簡單的技巧來理解會發生什麼:

  1. 在函數的開頭插入行result = []
  2. result.append(expr)替換每個yield expr
  3. 在函數底部插入一行return result
  4. 耶 - 沒有更多的yield聲明! 閱讀並找出代碼。
  5. 比較功能與原始定義。

這個技巧可以讓你了解函數背後的邏輯,但是實際上與yield與基於列表的方法中發生的情況有很大不同。 在許多情況下,yield方法將更高效,更快。 在其他情況下,即使原始函數工作得很好,這個技巧也會讓你陷入無限循環。 請繼續閱讀以了解更多信息...

不要混淆你的Iterables,Iterators和Generators

首先, 迭代器協議 - 當你寫

for x in mylist:
    ...loop body...

Python執行以下兩個步驟:

  1. 獲取mylist的迭代器:

    調用iter(mylist) - >這將返回一個帶有next()方法的對象(或Python 3中的__next__() )。

    [這是大多數人忘記告訴你的步驟]

  2. 使用迭代器循環遍歷項目:

    繼續調用從步驟1返回的迭代器上的next()方法。將next()的返回值賦給x並執行循環體。 如果從next()引發異常StopIteration ,則意味著迭代器中沒有更多值,並且退出循環。

事實上,Python在任何時候想要循環對象的內容時執行上述兩個步驟 - 所以它可能是for循環,但它也可能是像otherlist.extend(mylist)代碼( otherlist列表是Python列表) 。

這裡mylist是一個可迭代的,因為它實現了迭代器協議。 在用戶定義的類中,您可以實現__iter__()方法以使類的實例可迭代。 此方法應返回迭代器 。 迭代器是一個帶有next()方法的對象。 可以在同一個類上實現__iter__()next() ,並使__iter__()返回self 。 這適用於簡單的情況,但是當您希望兩個迭代器同時循環遍歷同一個對象時。

所以這是迭代器協議,許多對象實現了這個協議:

  1. 內置列表,詞典,元組,集,文件。
  2. 用戶定義的實現__iter__()
  3. 發電機。

請注意, for循環不知道它正在處理什麼類型的對象 - 它只是遵循迭代器協議,並且很樂意在它調用next()獲得項目。 內置列表逐個返回它們的項目,字典逐個返回 ,文件一個接一個地返回等。然後生成器返回......那就是yield來源:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

如果你在f123()有三個return語句而不是yield語句,那麼只會執行第一個語句,並且函數將退出。 但是f123()不是普通的功能。 當f123() ,它返回yield語句中的任何值! 它返回一個生成器對象。 此外,該功能並沒有真正退出 - 它進入暫停狀態。 當for循環試圖遍歷生成器對象時,該函數在之前返回的yield之後的下一行從其掛起狀態恢復,執行下一行代碼,在本例中為yield語句,並將其返回為下一個項目。 這種情況一直發生,直到函數退出,此時生成器引發StopIteration ,循環退出。

所以生成器對像有點像適配器 - 它的一端展示了迭代器協議,通過暴露__iter__()next()方法來保持for循環的快樂。 然而,在另一端,它運行該功能足以從中獲取下一個值,並將其重新置於掛起模式。

為什麼要使用發電機?

通常,您可以編寫不使用生成器但實現相同邏輯的代碼。 一種選擇是使用我之前提到的臨時列表'技巧'。 這在所有情況下都不起作用,例如,如果你有無限循環,或者當你有一個很長的列表時,它可能會使內存的使用效率低下。 另一種方法是實現一個新的可迭代類SomethingIter ,它將狀態保存在實例成員中,並在它的next() (或Python 3中的__next__() )方法中執行下一個邏輯步驟。 根據邏輯, next()方法中的代碼可能看起來非常複雜並且容易出錯。 這裡的發電機提供了一個簡潔的解決方案


yield就像一個函數的返回元素。不同之處在於,yield元素將函數轉換為生成器。在某些東西“屈服”之前,生成器的行為就像一個函數。發電機停止,直到下一次調用,並從它開始的完全相同的點繼續。您可以通過調用將所有“已產生”值的序列合二為一list(generator())


yield關鍵字簡化為兩個簡單的事實:

  1. 如果編譯器在函數內的任何位置檢測到yield關鍵字,則該函數不再通過return語句return相反 ,它會立即返回一個稱為生成器的惰性“掛起列表”對象
  2. 生成器是可迭代的。 什麼是可迭代的 ? 它類似於listsetrange或字典視圖,具有用於按特定順序訪問每個元素內置協議

簡而言之: 生成器是一個惰性的,遞增掛起的列表yield語句允許您使用函數表示法來編程生成器應逐漸吐出的列表值

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

讓我們定義一個函數makeRange就像Python的range 。 調用makeRange(n)發生器:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

要強制生成器立即返回其掛起值,您可以將其傳遞給list() (就像您可以任意迭代一樣):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

比較“只返回列表”的示例

上面的例子可以被認為只是創建一個你追加並返回的列表:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

但是有一個主要的區別; 見最後一節。

你如何使用發電機

可迭代是列表推導的最後一部分,並且所有生成器都是可迭代的,因此它們經常被使用:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

為了更好地了解生成器,您可以使用itertools模塊(確保在保證時使用chain.from_iterable而不是chain )。 例如,您甚至可以使用生成器來實現無限長的惰性列表,例如itertools.count() 。 您可以實現自己的def enumerate(iterable): zip(count(), iterable) ,或者在while循環中使用yield關鍵字執行此操作。

請注意:生成器實際上可以用於更多的事情,例如實現協同程序或非確定性編程或其他優雅的東西。 但是,我在這裡提出的“懶惰列表”觀點是您將找到的最常見的用途。

在幕後

這就是“Python迭代協議”的工作原理。 也就是說, list(makeRange(5))時會發生什麼list(makeRange(5)) 。 這就是我之前描述的“懶惰的增量列表”。

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

內置函數next()只調用對象.next()函數,它是“迭代協議”的一部分,可以在所有迭代器上找到。 您可以手動使用next()函數(以及迭代協議的其他部分)來實現花哨的東西,通常以犧牲可讀性為代價,因此盡量避免這樣做......

細節

通常情況下,大多數人不會關心以下區別,可能想在這裡停止閱讀。

在Python中, 可迭代是任何“理解for循環的概念”的對象,如列表[1,2,3]迭代器是所請求的for循環的特定實例,如[1,2,3].__iter__()生成器與任何迭代器完全相同,除了它的編寫方式(使用函數語法)。

從列表中請求迭代器時,它會創建一個新的迭代器。 但是,當您從迭代器(您很少這樣做)請求迭代器時,它只會為您提供自身的副本。

因此,萬一你沒有做到這樣的事情......

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

...然後記住發電機是一個迭代器 ; 也就是說,它是一次性的。 如果要重複使用它,則應再次調用myRange(...) 。 如果需要使用結果兩次,請將結果轉換為列表並將其存儲在變量x = list(myRange(5)) 。 那些絕對需要克隆生成器的人(例如,誰正在做可怕的hackish元編程)可以使用itertools.tee如果絕對必要,因為可複制的迭代器Python PEP標準提案已被推遲。


收益率是一個對象

return函數中的A 將返回單個值。

如果您希望函數返回一組大量值,請使用yield

更重要的yield是,是一個障礙

就像CUDA語言中的屏障一樣,它不會在控製完成之前進行轉移。

也就是說,它將從頭開始運行代碼,直到它命中yield。然後,它將返回循環的第一個值。

然後,每隔一個調用將再次運行您在函數中編寫的循環,返回下一個值,直到沒有任何值返回。


下面是一些如何實際實現生成器的Python示例,就好像Python沒有為它們提供語法糖:

作為Python生成器:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

使用詞法閉包而不是生成器

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

使用對象閉包而不是生成器(因為ClosuresAndObjectsAreEquivalent

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

想一想:

對於具有next()方法的對象,迭代器只是一個奇特的聲音術語。 因此,屈服函數最終會像這樣:

原始版本:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

這基本上是Python解釋器對上面代碼的作用:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

為了更深入地了解幕後發生的事情,可以將for循環重寫為:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

這更有意義還是只是讓你感到困惑? :)

我應該注意到,這僅僅是為了說明目的而過於簡單化。 :)


我將發布“閱讀Beazley的'Python:Essential Reference'第19頁,以便快速描述發生器”,但是很多其他人已經發布了很好的描述。

另外,請注意,yield可以在協同程序中使用它們作為它們在生成器函數中的雙重使用。雖然它與您的代碼片段的用法不同,(yield)但可以用作函數中的表達式。當調用者使用該send()方法向方法發送值時,協程將執行,直到(yield)遇到下一個語句。

生成器和協同程序是設置數據流類型應用程序的一種很酷的方法。我認為yield在函數中知道語句的其他用法是值得的。


產量為您提供發電機。

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

如您所見,在第一種情況下,foo會立即將整個列表保存在內存中。對於包含5個元素的列表來說,這不是什麼大問題,但是如果你想要一個500萬的列表呢?這不僅是一個巨大的內存消耗者,而且在調用函數時也需要花費大量時間來構建。在第二種情況下,bar只給你一個發電機。生成器是可迭代的 - 這意味著您可以在for循環等中使用它,但每個值只能被訪問一次。所有值也不會同時存儲在內存中;生成器對象“記住”上次調用它時循環的位置 - 這樣,如果你使用一個可迭代(比如說)計數到500億,那麼你不需要數到500億全部立刻存儲500億個數字。再次,這是一個非常人為的例子,如果你真的想要數到500億,你可能會使用itertools。 :)

這是生成器最簡單的用例。正如你所說,它可以用來編寫有效的排列,使用yield來通過調用堆棧推送,而不是使用某種堆棧變量。生成器也可以用於專門的樹遍歷,以及其他各種方式。


要了解yield作用,您必須了解發電機是什麼。 在發電機出現之前。

Iterables

創建列表時,您可以逐個閱讀其項目。 逐個讀取它的項稱為迭代:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist是一個可迭代的 。 當您使用列表推導時,您創建一個列表,因此是一個可迭代的:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

您可以使用“ for... in... ”的所有內容都是可迭代的; listsstrings ,文件......

這些迭代很方便,因為您可以根據需要讀取它們,但是您將所有值存儲在內存中,當您擁有大量值時,這並不總是您想要的。

發電機

生成器是迭代器,是一種只能迭代一次的迭代器。 生成器不會將所有值存儲在內存中, 它們會動態生成值

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

除了你使用()而不是[]之外,它是一樣的。 但是,你不能 for i in mygenerator執行for i in mygenerator因為生成器只能使用一次:它們計算0,然後忘記它併計算1,然後逐個計算4。

產量

yield是一個像return一樣使用的關鍵字,除了函數將返回一個生成器。

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

這是一個無用的例子,但是當你知道你的函數將返回一組你只需要閱讀一次的大量值時它會很方便。

要掌握yield ,您必須明白, 當您調用函數時,您在函數體中編寫的代碼不會運行。 該函數只返回生成器對象,這有點棘手:-)

然後,每次使用生成器時,都會運行代碼。

現在困難的部分:

第一次調用從函數創建的生成器對象時,它將從頭開始運行函數中的代碼,直到達到yield ,然後它將返回循環的第一個值。 然後,每個其他調用將再次運行您在函數中寫入的循環,並返回下一個值,直到沒有值返回。

一旦函數運行,該生成器被認為是空的,但不再達到yield 。 這可能是因為循環已經結束,或者因為你不再滿足"if/else"

你的代碼解釋了

發電機:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

呼叫者:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidates list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

此代碼包含幾個智能部分:

  • 循環在列表上迭代,但是循環迭代時列表會擴展:-)這是一個簡單的方法來遍歷所有這些嵌套數據,即使它有點危險,因為你最終可以得到一個無限循環。 在這種情況下, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))耗盡了生成器的所有值,但while不斷創建新的生成器對象,這些對象將生成與之前的值不同的值,因為它不應用於相同的值節點。

  • extend()方法是一個列表對象方法,它需要一個iterable並將其值添加到列表中。

通常我們將列表傳遞給它:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

但是在你的代碼中它得到了一個生成器,這很好,因為:

  1. 您不需要兩次讀取值。
  2. 您可能有很多孩子,並且您不希望它們都存儲在內存中。

它的工作原理是因為Python不關心方法的參數是否是列表。 Python期望iterables所以它將適用於字符串,列表,元組和生成器! 這叫做鴨子打字,這也是Python如此酷的原因之一。 但這是另一個故事,另一個問題......

你可以在這裡停下來,或者閱讀一下看看發電機的高級用途:

控制發電機的耗盡

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

注意:對於Python 3,使用print(corner_street_atm.__next__())print(next(corner_street_atm))

它可以用於控制對資源的訪問等各種事情。

Itertools,你最好的朋友

itertools模塊包含操作iterables的特殊函數。 曾經希望復制發電機嗎? 鏈兩個發電機? 使用單行分組嵌套列表中的值? Map / Zip而不創建另一個列表?

然後只需import itertools

一個例子? 讓我們來看看四匹馬比賽的可能到達順序:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

理解迭代的內在機制

迭代是一個暗示迭代(實現__iter__()方法)和迭代器(實現__next__()方法)的過程。 Iterables是可以從中獲取迭代器的任何對象。 迭代器是允許您迭代迭代的對象。

在這篇文章中有關於for循環如何工作的更多信息


像每個答案所暗示的那樣,yield用於創建序列生成器。它用於動態生成一些序列。例如,在網絡上逐行讀取文件時,可以使用以下yield函數:

def getNextLines():
   while con.isOpen():
       yield con.read()

您可以在代碼中使用它,如下所示:

for line in getNextLines():
    doSomeThing(line)

執行控制轉移問題

執行foryield時,執行控件將從getNextLines()傳送到循環。因此,每次調用getNextLines()時,執行都從上次暫停的位置開始。

因此簡而言之,具有以下代碼的功能

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

將打印

"first time"
"second time"
"third time"
"Now some useful value 12"

在我描述如何使用發電機的許多重要答案中,有一種我認為尚未給出的答案。這是編程語言理論的答案:

yieldPython中的語句返回一個生成器。Python中的生成器是一個返回continuation的函數(特別是一種coroutine,但continuation代表了更常用的機制來理解正在發生的事情)。

編程語言理論的延續是一種更為基礎的計算,但它們並不經常使用,因為它們極難推理並且也很難實現。但是,延續的概念很簡單:計算的狀態還沒有完成。在此狀態下,將保存變量的當前值,尚未執行的操作等。然後在程序的某個時刻,可以調用continuation,以便程序的變量重置為該狀態,並執行保存的操作。

以這種更一般的形式,可以以兩種方式實現連續。在call/cc方式,程序的堆棧字面上保存,然後調用延續時,堆棧恢復。

在連續傳遞樣式(CPS)中,continuation只是普通函數(僅在函數是第一類的語言中),程序員明確地管理它並傳遞給子例程。在這種風格中,程序狀態由閉包(以及碰巧在其中編碼的變量)表示,而不是駐留在堆棧中某處的變量。管理控制流的函數接受繼續作為參數(在CPS的某些變體中,函數可以接受多個延續)並通過簡單地調用它們並在之後返回來調用它們來操縱控制流。延續傳遞樣式的一個非常簡單的例子如下:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

在這個(非常簡單的)示例中,程序員將實際寫入文件的操作保存到一個延續中(這可能是一個非常複雜的操作,需要寫出許多細節),然後傳遞該延續(即,作為第一個 - class closure)到另一個執行更多處理的運算符,然後在必要時調用它。(我在實際的GUI編程中經常使用這種設計模式,因為它節省了我的代碼行,或者更重要的是,在GUI事件觸發後管理控制流。)

本文的其餘部分將不失一般性地將延續概念化為CPS,因為它更容易理解和閱讀。


現在讓我們談談Python中的生成器。生成器是延續的特定子類型。雖然continuation通常能夠保存計算的狀態(即程序的調用堆棧),但生成器只能通過迭代器保存迭代狀態。雖然這個定義對於某些發電機的使用情況略有誤導。例如:

def f():
  while True:
    yield 4

這顯然是一個合理的迭代,其行為很明確 - 每次生成器迭代它,它返回4(並且永遠這樣做)。但是,在考慮迭代器(即,for x in collection: do_something(x))時,它可能不是想到的典型迭代類型。這個例子說明了生成器的強大功能:如果有什麼是迭代器,生成器可以保存其迭代的狀態。

重申:Continuations可以保存程序堆棧的狀態,生成器可以保存迭代狀態。這意味著continuation比生成器更強大,但是生成器也很多,更容易。它們對於語言設計者來說更容易實現,並且程序員更容易使用它們(如果你有時間刻錄,嘗試閱讀和理解這個頁面關於continuation和call / cc)。

但是您可以輕鬆地實現(和概念化)生成器作為連續傳遞樣式的簡單特定情況:

無論什麼時候yield被調用,它都會告訴函數返回一個延續。再次調用該函數時,它從它停止的任何地方開始。因此,在偽偽代碼(即,不是偽代碼,而不是代碼)中,生成器的next方法基本如下:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

其中yield關鍵字實際上是真正的生成器函數的語法糖,基本上類似於:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

請記住,這只是偽代碼,Python中生成器的實際實現更複雜。但是,作為了解正在發生的事情的練習,嘗試使用延續傳遞樣式來實現生成器對象而不使用yield關鍵字。


對於那些喜歡最小工作示例的人,請冥想這個交互式Python會話:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed

從編程的角度來看,迭代器實現為thunks

要將並發執行的迭代器,生成器和線程池等實現為thunks(也稱為匿名函數),可以使用發送給具有調度程序的閉包對象的消息,並且調度程序將回答“消息”。

http://en.wikipedia.org/wiki/Message_passing

next ”是發送到閉包的消息,由“ iter ”調用創建。

有很多方法可以實現這個計算。我使用了變異,但通過返回當前值和下一個yielder,很容易做到沒有變異。

這是一個使用R6RS結構的演示,但語義與Python完全相同。它是相同的計算模型,只需要在Python中重寫它就需要改變語法。

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->

許多人使用return而不是yield,但在某些情況下yield可以更有效,更容易使用。

這是一個yield絕對最適合的例子:

返回(在功能中)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

產量(功能)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

調用函數

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

這兩個函數都做同樣的事情,但yield使用三行而不是五行,並且有一個較少的變量需要擔心。

這是代碼的結果:

正如您所看到的,兩個函數都做同樣的事情。唯一的區別是return_dates()給出一個列表並yield_dates()給出一個生成器。

一個現實生活中的例子就像是逐行讀取文件或者只是想製作一個生成器。


yield關鍵字簡單地收集返回結果。想想yield就好return +=


這是一個簡單yield的方法,用於計算斐波納契系列,解釋如下:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

當你在REPL中輸入它然後嘗試調用它時,你會得到一個神秘的結果:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

這是因為yield您希望創建生成器,即根據需要生成值的對象,向Python發出信號。

那麼,你如何生成這些值?這可以通過使用內置函數直接完成,也可以next通過將其提供給消耗值的構造來間接完成。

使用內置next()函數,直接調用.next/ __next__,強制生成器生成一個值:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

間接地,如果你提供fib一個for循環,一個list初始化器,一個tuple初始化器或任何其他需要生成/生成值的對象的東西,你將“消耗”生成器,直到它不再生成值(並返回) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

同樣,使用tuple初始化程序:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

生成器與函數的不同之處在於它是惰性的。它通過維護本地狀態並允許您隨時恢復來實現此目的。

當您第一次fib通過調用它來調用時:

f = fib()

Python編譯函數,遇到yield關鍵字並簡單地返回一個生成器對象。似乎不是很有幫助。

然後,當您請求它直接或間接生成第一個值時,它會執行它找到的所有語句,直到遇到a yield,然後它會返回您提供給的值yield並暫停。有關更好地演示此示例的示例,讓我們使用一些print調用(print "text"在Python 2上替換為if):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

現在,輸入REPL:

>>> gen = yielder("Hello, yield!")

你有一個生成器對象現在正在等待命令讓它生成一個值。使用next並查看打印內容:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

未加引號的結果是印刷的。引用的結果是從中返回的結果yieldnext現在再打電話:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

發電機記得它暫停yield value並從那裡恢復。打印下一條消息yield,再次執行搜索暫停的語句(由於while循環)。


這是一個簡單語言的例子。我將提供高級人類概念與低級Python概念之間的對應關係。

我想對一系列數字進行操作,但我不想因為創建該序列而煩惱我自己,我只想專注於我想要做的操作。所以,我做了以下事情:

  • 我打電話給你,告訴你我想要一個以特定方式產生的數字序列,我告訴你算法是什麼。
    該步驟對應於def生成器函數,即包含a的函數yield
  • 過了一會兒,我告訴你,“好的,準備告訴我數字的順序”。
    此步驟對應於調用返回生成器對象的生成器函數。請注意,你還沒有告訴我任何數字; 你抓住你的紙和鉛筆。
  • 我問你,“告訴我下一個號碼”,你告訴我第一個號碼; 之後,你等我問你下一個號碼。這是你的工作,要記住你在哪裡,你已經說過的數字,以及下一個數字是什麼。我不關心細節。
    此步驟對應於調用.next()生成器對象。
  • ...重複上一步,直到......
  • 最終,你可能會走到盡頭。你沒告訴我一個號碼; 你只是喊道,“抓住你的馬!我已經完成了!沒有更多的數字!”
    此步驟對應於生成器對象結束其作業,並引發StopIteration異常生成器函數不需要引發異常。當函數結束或發出時,它會自動引發return

這就是生成器所做的事情(包含a的函數yield); 它開始執行,只要它執行一次就暫停yield,當被要求輸入一個.next()值時,它會從最後一次繼續執行。它完全符合Python的迭代器協議設計,它描述瞭如何順序請求值。

迭代器協議最著名的用戶是forPython中的命令。所以,每當你做一個:

for item in sequence:

如果sequence是如上所述的列表,字符串,字典或生成器對象並不重要; 結果是一樣的:你逐個讀取序列中的項目。

請注意,def包含yield關鍵字的函數不是創建生成器的唯一方法; 這只是創建一個最簡單的方法。

有關更準確的信息,請閱讀Python文檔中的迭代器類型yield語句generators


還有另一種yield用途和含義(自Python 3.3起):

yield from <expr>

PEP 380 - 委託給子發電機的語法

提出了一種語法,用於生成器將其部分操作委託給另一個生成器。這允許將包含'yield'的一段代碼分解出來並放在另一個生成器中。此外,允許子生成器返回一個值,該值可供委派生成器使用。

當一個生成器重新生成另一個生成器生成的值時,新語法也為優化提供了一些機會。

此外,this將介紹(自Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

避免協程與常規生成器混淆(今天yield兩者都使用)。


雖然很多答案都說明了為什麼要使用a yield創建生成器,但有更多的用途yield。製作協程非常容易,它可以在兩個代碼塊之間傳遞信息。我不會重複已經給出的關於使用yield創建生成器的任何精細示例。

為了幫助理解yield以下代碼中的功能,您可以用手指在任何具有代碼的代碼中跟踪循環yield。每當你的手指擊中時yield,你必須等待a next或a send進入。當a next被調用時,你會遍歷代碼,直到你點擊yield... yield評估右側的代碼並返回給調用者...然後你等待。當next被再次調用,您通過代碼進行另一次循環。但是,您會注意到在協程中,yield也可以與send... 一起使用,它會將調用者的值發送讓步函數中。如果send給出了a ,那麼yield接收發送的值,並將其從左側吐出...然後通過代碼的跟踪進行直到yield再次擊中(在結束時返回值,就像next被調用一樣)。

例如:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()




coroutine