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

What does the “yield” keyword do?
投票:9614回答:38

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)編寫的,Jochen Schulz是一個很好的用於度量空間的Python庫。 這是完整源代碼的鏈接: Module mspace

tags:python,iterator,generator,yield,coroutine
38回答
93
投票

以下是一些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)

173
投票

產量可以為您提供發電機。

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可以將內容推入調用堆棧,而不是使用某種堆棧變量。 生成器還可以用於特殊的樹遍歷以及所有其他方式。


202
投票

對於那些偏愛簡單工作示例的人,請在此交互式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

87
投票

我本打算發布“閱讀Beazley的“ Python:基本參考”的第19頁,以快速了解生成器”,但是已經有許多其他人發布了不錯的描述。

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

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


152
投票

在描述如何使用生成器的許多很棒的答案中,我還沒有給出一種答案。 這是編程語言理論的答案:

Python中的yield語句返回一個生成器。 (and specifically a type of coroutine, but continuations represent the more general mechanism to understand what is going on). Python中的生成器是一個返回的函數(特別是協程類型,但是延續代表了一種更通用的機制來了解正在發生的事情)。

編程語言理論中的連續性是一種更為基礎的計算,但是由於它們很難推理而且也很難實現,因此並不經常使用。 但是,關於延續是什麼的想法很簡單:只是尚未完成的計算狀態。 在此狀態下,將保存變量的當前值,尚未執行的操作等。 然後,在稍後的某個時刻,可以在程序中調用繼續,以便將程序的變量重置為該狀態,並執行保存的操作。

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

在延續傳遞樣式(CPS)中,延續只是普通的函數(僅在函數為第一類的語言中),程序員明確地對其進行管理並傳遞給子例程。 以這種方式,程序狀態由閉包(以及恰好在其中編碼的變量)表示,而不是駐留在堆棧中某個位置的變量。 管理控制流的函數接受連續作為參數(在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)

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

在不失一般性的前提下,本文的其餘部分將連續性概念化為CPS,因為它很容易理解和閱讀。


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

def f():
  while True:
    yield 4

顯然,這是一個合理的迭代器,其行為已得到很好的定義-每次生成器對其進行迭代時,它都會返回4(並永遠這樣做)。 但是在考慮迭代器時(例如, for x in collection: do_something(x) ),可能不會想到原型的可迭代類型。 此示例說明了生成器的功能:如果有什麼是迭代器,生成器可以保存其迭代狀態。

重申一下:連續可以保存程序堆棧的狀態,而生成器可以保存迭代的狀態。 這意味著延續比生成器強大得多,但是生成器也非常簡單。 它們對於語言設計者來說更容易實現,對程序員來說也更容易使用(如果您有時間要燃燒,請嘗試閱讀並理解有關延續和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關鍵字。


61
投票

這是yield的心理印象。

我喜歡將線程視為具有堆棧(即使未以這種方式實現)。

調用普通函數時,它將其局部變量放在堆棧上,進行一些計算,然後清除堆棧並返回。 再也看不到其局部變量的值。

使用yield函數,當其代碼開始運行時(即,在調用該函數之後,返回生成器對象,然後調用next()方法的生成器對象),它類似地將其局部變量放入堆棧中並進行一段時間的計算。 但是,當它命中yield語句時,在清除堆棧的一部分並返回之前,它會對其局部變量進行快照並將其存儲在生成器對像中。 它還在代碼中寫下了當前位置(即特定的yield語句)。

因此,這是生成器掛起的一種凍結函數。

隨後調用next() ,它將函數的所有物檢索到堆棧上並對其進行動畫處理。 該函數從中斷處繼續進行計算,而忽略了它剛剛在冷庫中度過了一個永恆的事實。

比較以下示例:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

當我們調用第二個函數時,它的行為與第一個函數非常不同。 yield語句可能無法到達,但是如果它出現在任何地方,它將改變我們正在處理的內容的性質。

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

調用yielderFunction()不會運行其代碼,而是使代碼生成器。 (為便於閱讀,用yielder前綴命名此類名稱可能是個好主意。)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

gi_codegi_frame字段是凍結狀態的存儲位置。 dir(..)探索它們,我們可以確認我們上面的思維模型是可信的。


87
投票

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

為了將迭代器,生成器和線程池實現為並發執行等,作為重擊(也稱為匿名函數),人們使用發送到具有分派器的閉包對象的消息,然後分派器對“消息”做出響應。

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

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

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

這是一個使用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) '(ab)) 'OK -> (stream 'yield) 'a -> (stream 'yield) 'b -> (stream 'yield) 'END -> (stream 'yield) 'END -> 

77
投票

這是一個簡單的示例:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

輸出:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

我不是Python開發人員,但在我看來, yield保持程序流的位置,而下一個循環從“ yield”位置開始。 似乎它正在那個位置等待,就在那之前,在外面返回一個值,下一次繼續工作。

這似乎是一種有趣而又不錯的能力:D


119
投票

儘管很多答案都說明了為什麼要使用yield來創建生成器,但是yield有更多用途。 創建協程非常容易,這使信息可以在兩個代碼塊之間傳遞。 我不會重複任何關於使用yield生成生成器的出色示例。

為了幫助理解以下代碼中的yield ,您可以用手指在任何具有yield代碼中跟踪循環。 每一個手指打的時候yield ,你必須等待一個nextsend到輸入。 next被調用,您通過跟踪代碼,直到你遇到yield ...在右邊的代碼yield進行評估,並返回給調用者...那你就等著。 再次調用next時,您將通過代碼執行另一個循環。 但是,您會注意到,在協程中, yield也可以與send一起使用,它將從調用方將值發送 yielding函數。 如果給出了send ,則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()

297
投票

yield就像return一樣-它返回您告訴的任何內容(作為生成器)。 不同之處在於,下次調用生成器時,執行從最後一次調用到yield語句開始。 與return不同的是,在產生良率時不會清除堆棧幀,但是會將控制權轉移回調用者,因此下次調用該函數時,其狀態將恢復。

對於您的代碼,函數get_child_candidates的行為就像一個迭代器,因此當您擴展列表時,它一次將一個元素添加到新列表中。

list.extend調用迭代器,直到耗盡為止。 對於您發布的代碼示例,返回一個元組並將其附加到列表中會更加清楚。


164
投票

它正在返回發電機。 我對Python並不是特別熟悉,但是如果您熟悉C#的迭代器塊 ,我相信它與C#的迭代器塊一樣

關鍵思想是,編譯器/解釋器/無論做什麼都做一些技巧,以便就調用者而言,他們可以繼續調用next(),並且將繼續返回值- 就像Generator方法已暫停一樣 現在顯然您不能真正地“暫停”方法,因此編譯器構建了一個狀態機,供您記住您當前所在的位置以及局部變量等的外觀。 這比自己編寫迭代器要容易得多。


506
投票

這樣想:

迭代器只是一個具有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

這是否更有意義,還是會讓您更加困惑? :)

我要指出,這為了說明的目的過於簡單化。 :)


13916
投票

要了解什麼是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...上使用“ 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 ,您必須了解在調用函數時,在函數主體中編寫的代碼不會運行。 該函數僅返回生成器對象,這有點棘手:-)

然後,您的代碼將繼續從它每次離開的地方for使用發電機。

現在最困難的部分是:

for第一次調用從您的函數創建的生成器對象時,它將從頭開始運行函數中的代碼,直到達到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 the 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 the 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 candidate's 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()方法是一個列表對象方法,該方法需要可迭代並將其值添加到列表中。

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

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

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

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

它之所以有效,是因為Python不在乎方法的參數是否為列表。 Python期望可迭代,因此它將與字符串,列表,元組和生成器一起使用! 這就是所謂的鴨子輸入,這是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模塊包含用於操縱可迭代對象的特殊功能。 曾經希望復制一個發電機嗎? 連鎖兩個發電機? 用一個單行將嵌套列表中的值分組? 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__()方法)的過程。 可迭代對像是可以從中獲取迭代器的任何對象。 迭代器是使您可以迭代的對象。

本文中有更多關於for循環如何工作的內容


134
投票

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

我想對數字序列進行運算,但是我不想為創建該序列而煩惱自己,我只想著重於自己想做的運算。 因此,我執行以下操作:

  • 我打電話給你,告訴你我想要一個以特定方式產生的數字序列,讓您知道算法是什麼。
    該步驟對應於def進不去發電機功能,即包含在功能yield
  • 稍後,我告訴您,“好,準備告訴我數字的順序”。
    此步驟對應於調用生成器函數,該函數返回生成器對象。 請注意,您還沒有告訴我任何數字。 你只要拿起紙和鉛筆。
  • 我問你,“告訴我下一個號碼”,然後你告訴我第一個號碼; 之後,您等我問您下一個電話號碼。 記住您的位置,已經說過的電話號碼以及下一個電話號碼是您的工作。 我不在乎細節。
    此步驟對應於在生成器對像上調用.next()
  • …重複上一步,直到…
  • 最終,您可能會走到盡頭。 你不告訴我電話號碼; 您只是大聲喊道:“抱馬!我做完了!沒有數字了!”
    此步驟對應於生成器對象結束其工作,並引發StopIteration異常 。生成器函數不需要引發異常。 函數結束或發出return時,它將自動引發。

生成器就是這樣做的(包含yield的函數); 它開始執行,在執行yield時暫停,並在詢問.next()值時從上一個點繼續。 根據設計,它與Python的迭代器協議完美契合,該協議描述瞭如何順序請求值。

迭代器協議最著名的用戶是Python中的for命令。 因此,無論何時執行以下操作:

for item in sequence:

如果不要緊sequence是一個列表,一個字符串,一個詞典或發電機對象等如上所述; 結果是相同的:您從一個序列中逐個讀取項目。

需要注意的是def進不去其中包含一個函數yield關鍵字不是產生一個生成的唯一途徑; 這是創建一個的最簡單的方法。

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


222
投票

還有另外一件事要提及:yield的函數實際上不必終止。 我寫了這樣的代碼:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

然後我可以在其他代碼中使用它:

for f in fib():
    if some_condition: break
    coolfuncs(f);

它確實有助於簡化某些問題,並使某些事情更易於使用。


1882
投票

了解yield捷徑

當您看到帶有yield語句的函數時,請應用以下簡單技巧,以了解將發生的情況:

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

這個技巧可能會讓您對函數背後的邏輯有所了解,但是yield實際發生的情況與基於列表的方法發生的情況顯著不同。 在許多情況下,yield方法也將大大提高內存效率和速度。 在其他情況下,即使原始函數運行正常,此技巧也會使您陷入無限循環。 請繼續閱讀以了解更多信息...

不要混淆您的Iterable,Iterators和Generators

首先, 迭代器協議 -當您編寫時

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

Python執行以下兩個步驟:

  1. 獲取mylist的迭代器:

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

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

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

    保持調用next()從第1步中從返回的返回值的迭代方法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()方法內的代碼可能最終看起來非常複雜,並且容易出現錯誤。 在這裡,發電機提供了一種干淨而簡單的解決方案。


112
投票

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

yield from <expr>

PEP 380-委託給子生成器的語法

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

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

此外, 將引入(自Python 3.5起):

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

以避免將協程與常規生成器混淆(兩者均使用了今天的yield )。


38
投票

yield就像函數的返回元素一樣。 不同之處在於, yield元素將函數轉換為生成器。 生成器的行為就像一個函數,直到“屈服”為止。 生成器停止運行,直到下一次調用為止,並從與啟動完全相同的點繼續運行。 您可以通過調用list(generator())獲得一個包含所有“屈服”值的序列。


342
投票

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

答案大綱/摘要

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

發電機:

yield僅在函數定義內部是合法的,並且yield包含在函數定義中使其返回生成器。

生成器的想法來自具有不同實現方式的其他語言(請參見腳註1)。 在Python的Generators中,代碼的執行會在收益率點凍結 調用生成器時(下面將討論方法),恢復執行,然後凍結下一個收益率。

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來使生成器排隊。 根據您使用的Python版本,它將調用適當的next__next__方法:

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

合作派遣到協程, yield from

現在,回想一下yield from在Python 3中可用。這使我們可以將協程委託給子協程:

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具有返回iterator__iter__方法。 迭代器提供.next (Python 2或.__next__ (Python 3))方法,該方法由for循環隱式調用,直到引發StopIteration為止,一旦這樣做,它將繼續這樣做。
  • 然後,它使用生成器表達式來描述什麼是生成器。 由於生成器只是創建迭代器的一種簡便方法,因此它只會使問題感到困惑,而我們仍未達到yield部分。
  • 控制發電機的排氣中,他調用.next方法,而應改為使用內置函數next 這將是一個適當的間接層,因為他的代碼在Python 3中不起作用。
  • Itertools? 這根本與yield無關。
  • 沒有討論yield的方法以及Python 3中新功能的yield from最高/可接受的答案是非常不完整的答案。

暗示對生成器表達或理解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

由於yield是一種表達,因此儘管沒有特別好的用例,但有人認為它可以用於理解或生成器表達中。

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

2017年1月30日19:05,布雷特·坎農寫道:

2017年1月29日星期日,克雷格·羅德里格斯(Craig Rodrigues)在星期日寫道:

兩種方法我都可以。 恕我直言,把事情留在Python 3中是不好的。

我的投票是SyntaxError,因為您沒有從語法中得到期望。

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

關於到達那裡,我們可能想要:

  • 3.7中的語法警告或棄用警告
  • 2.7.x中的Py3k警告
  • 3.8中的SyntaxError

乾杯,尼克。

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

此外,還有一個懸而未決的問題(10544) ,似乎正說明這絕不是一個好主意(PyPy,用Python編寫的Python實現,已經在發出語法警告。)

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

生成器中的return語句

Python 2中

在生成器函數中, return語句不允許包含expression_list 在這種情況下,簡單的return指示生成器已完成,並將導致StopIteration升高。

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

Python 3中

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

腳註

  1. 提案中引用了CLU,Sather和Icon語言,以將生成器的概念引入Python。 總體思路是,一個函數可以維護內部狀態並根據用戶的需要產生中間數據點。 這有望在性能上優於其他方法,包括Python線程 ,該方法甚至在某些系統上不可用。

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

  3. yield最初是作為語句引入的,這意味著它只能出現在代碼塊的一行的開頭。 現在yield創建一個yield表達式。 https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt 提出此更改是為了允許用戶將數據發送到生成器中,就像接收數據一樣。 要發送數據,必須能夠將其分配給某物,為此,一條語句就行不通了。


52
投票

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

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

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

for line in getNextLines():
    doSomeThing(line)

執行控制轉移陷阱

執行yield時,執行控制將從getNextLines()轉移到for循環。 因此,每次調用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"

46
投票

產量是一個對象

return在一個函數會返回一個值。

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

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

就像CUDA語言中的barrier一樣,它在完成之前不會轉移控制權。

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

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


36
投票

yield關鍵字只是收集返回的結果。 yield視為return +=


33
投票

這是一種用於計算斐波那契數列的基於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發出信號,表示您想要創建一個generator ,即一個按需生成值的對象。

那麼,如何生成這些值? 這可以通過使用內置函數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關鍵字,然後簡單地將生成器對象返回給您。 看起來不是很有幫助。

然後,當您請求它直接或間接生成第一個值時,它將執行找到的所有語句,直到遇到yield ,然後將您提供的值返回給yield並暫停。 為了更好地說明這一點,讓我們使用一些print調用(如果在Python 2上,請用print "text"代替):

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!'

未報價的結果是所打印的內容。 引用的結果是yield所返回的結果。 呼叫next現在再次:

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


46
投票

(我下面的回答僅從使用Python生成器的角度講,而不是生成器機制基礎實現 ,它涉及堆棧和堆操作的一些技巧。)

當使用yield而不是python函數中的return時,該函數將變成一種特殊的generator function 該函數將返回generator類型的對象。 yield關鍵字是一個標誌,用於通知python編譯器專門對待此類功能。 普通函數將在返回一些值後終止。 但是在編譯器的幫助下, 可以將 generator函數視為可恢復的。 也就是說,將恢復執行上下文,並且將從上次運行繼續執行。 在顯式調用return之前,它將引發StopIteration異常(這也是迭代器協議的一部分),或到達函數的結尾。 我發現了很多關於引用的generator ,但這一個functional programming perspective是最消化的。

(現在,我想根據自己的理解來討論generator的基本原理,以及iterator 。我希望這可以幫助您掌握迭代器和generator的基本動機 。這種概念也出現在其他語言中,例如C#。)

據我了解,當我們要處理一堆數據時,通常先將數據存儲在某個地方,然後再逐一處理。 但是這種幼稚的方法是有問題的。 如果數據量巨大,則預先存儲它們是很昂貴的。 因此,為什麼不間接存儲某種類型的metadata ,而不是直接存儲data本身,即the logic how the data is computed

有兩種包裝此類元數據的方法。

  1. 面向對象的方法,我們將元數據包裝as a class 這就是實現迭代器協議的所謂iterator器(即__next__()__iter__()方法)。 這也是常見的迭代器設計模式
  2. 在函數方法中,我們將元數據包裝as a function 這就是所謂的generator function 但實際上,返回的generator object仍然IS-A迭代器,因為它也實現了迭代器協議。

無論哪種方式,都會創建一個迭代器,即某個可以為您提供所需數據的對象。 OO方法可能有點複雜。 無論如何,要使用哪一個取決於您。


187
投票

TL; DR

代替這個:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

做這個:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

當你發現自己從頭開始建立一個列表, yield每片來代替。

這是我第一次屈服。


yield是一種含糖的說法

建立一系列的東西

相同的行為:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

不同的行為:

收益是單次通過 :您只能迭代一次。 當一個函數包含一個yield時,我們稱其為Generator函數 迭代器就是它返回的內容。 這些術語在揭示。 我們失去了容器的便利性,但獲得了按需計算且任意長的序列的功效。

產量懶惰 ,推遲了計算。 當您調用函數時,其中包含yield的函數實際上根本不會執行。 它返回一個迭代器對象 ,該對象記住它從何處中斷。 每次您在迭代器上調用next() (這在for循環中發生)時,執行都會向前推進到下一個收益。 return引發StopIteration並結束序列(這是for循環的自然結束)。

產量多才多藝 數據不必全部存儲在一起,可以一次存儲一次。 它可以是無限的。

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

如果您需要多次通過並且序列不太長,只需在其上調用list()

>>> list(square_yield(4))
[0, 1, 4, 9]

明智地選擇yield因為兩種含義都適用:

產量 —生產或提供(如在農業中)

...提供系列中的下一個數據。

屈服 —讓步或放棄(如在政治權力中一樣)

...放棄CPU執行,直到迭代器前進。


45
投票

許多人使用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()提供了一個生成器。

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


46
投票

總而言之, yield語句將您的函數轉換為一個工廠,該工廠產生一個稱為generator的特殊對象,該對象環繞原始函數的主體。 迭代generator ,它將執行您的函數,直到達到下一個yield為止,然後掛起執行並評估傳遞給yield的值。 它將在每次迭代中重複此過程,直到執行路徑退出函數為止。 例如,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

簡單地輸出

one
two
three

動力來自將生成器與計算序列的循環配合使用,生成器每次執行循環都會停止,以“產生”下一個計算結果,這樣就可以即時計算列表,而好處是可以存儲保存用於特別大的計算

假設您要創建自己的range函數以產生可迭代的數字範圍,則可以這樣做,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

像這樣使用

for i in myRangeNaive(10):
    print i

但這是低效的,因為

  • 您創建只使用一次的數組(這會浪費內存)
  • 這段代碼實際上在該數組上循環了兩次! :(

幸運的是,Guido和他的團隊足夠慷慨地開發發電機,因此我們可以做到這一點。

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

現在,在每次迭代時,生成器上稱為next()的函數都會執行該函數,直到它到達“ yield”語句為止,在該語句中它停止並“ yield”值或到達函數的結尾。 在這種情況下,在第一次調用時, next()執行到yield語句並產生yield'n',在下一次調用時,它將執行遞增語句,跳回到'while',對其求值,如果為true,則執行將停止並再次產生“ n”,它將繼續這種方式,直到while條件返回false且生成器跳至函數末尾。


44
投票

一個簡單的例子來了解它是什麼: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print i

輸出為:

1 2 1 2 1 2 1 2

99
投票

所有好的答案,但是對於新手來說有點困難。

我認為您已經了解了return語句。

打個比方, returnyield是雙胞胎。 return表示“返回並停止”,而yield表示“返回但繼續”

  1. 嘗試獲取帶有return的num_list。
def num_list(n):
    for i in range(n):
        return i

運行:

In [5]: num_list(3)
Out[5]: 0

看,您只會得到一個數字,而不是列表。 return永遠不會讓你高高興興,只執行一次就退出。

  1. yield

yield替換return

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

現在,您將贏得所有數字。

相較於return曾經停,運行yield運行時間你刨。 您可以將return解釋為return one of them ,而yield則解釋為return all of them 這稱為iterable

  1. 我們可以再執行一步,用return重寫yield語句
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

這是yield的核心。

列表return輸出和對象yield輸出之間的區別是:

您將始終從列表對象獲取[0,1,2],但只能從“對象yield輸出”中檢索一次。 因此,它具有一個新的名稱generator對象,如Out[11]: <generator object num_list at 0x10327c990>

總之,作為一個隱喻,它可以:

  • returnyield是雙胞胎
  • listgenerator是雙胞胎

422
投票

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

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

簡而言之: 生成器是一個懶惰的,增量待定的list ,並且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)) 這就是我之前所說的“懶惰的增量列表”。

>>> 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)) 那些絕對需要克隆生成器的人(例如,正在進行駭人的駭人的元編程的人)可以在絕對必要的情況下使用itertools.tee ,因為可複制的迭代器Python PEP標準建議已被推遲。


©2020 sofbug.com - All rights reserved.