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




yield用法 yield用途 (25)

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


Answers

還有一件事需要提及:一個實際上不必終止收益的函數。我編寫了這樣的代碼:

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

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


總之,該yield語句將您的函數轉換為一個工廠,該工廠生成一個名為a的特殊對象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'語句,在該語句中它停止並“產生”該值或到達函數的末尾。在這種情況下,在第一次調用時,next()執行yield語句並生成'n',在下一次調用時它將執行increment語句,跳回'while',計算它,如果為true,它將停止並且再次屈服'n',它將繼續這樣,直到while條件返回false並且生成器跳轉到函數的末尾。


所有偉大的答案,但新手有點困難。

我假設你已經學會了這個return陳述。

作為類比,return並且yield是雙胞胎。return意味著'回歸和停止',而'產量'意味著'回歸,但繼續'

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

運行:

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

看,你只得到一個數字而不是它們的列表。return永遠不會讓你高興,只需實施一次並退出。

  1. 來了 yield

替換returnyield

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運行您計劃的時間。你可以理解returnreturn one of them,和yield作為return all of them。這叫做iterable

  1. 我們可以yield用另一個步驟重寫語句return
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>

總之,作為一個隱喻它的隱喻:

  • return並且yield是雙胞胎
  • list並且generator是雙胞胎

下面是一些如何實際實現生成器的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)

這是一個簡單的例子:

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


想一想:

對於具有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

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

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


這是一個什麼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(..),我們可以確認我們的上述心理模型是可信的。


這是一個簡單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循環)。


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

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並不是特別熟悉,但我相信它與C#的迭代器塊相同,如果你熟悉它們的話。

關鍵的想法是編譯器/解釋器/無論做什麼都有一些技巧,因此就調用者而言,他們可以繼續調用next()並且它將保持返回值 - 就好像生成器方法被暫停一樣。現在顯然你不能真正“暫停”一個方法,所以編譯器會建立一個狀態機,讓你記住你當前的位置以及局部變量等。這比自己編寫迭代器容易得多。


要了解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循環如何工作的更多信息


從編程的角度來看,迭代器實現為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
->

對於那些喜歡最小工作示例的人,請冥想這個交互式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

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

當在python函數中yield使用when 而不是a return時,該函數變成了一個特殊的函數generator function。該函數將返回一個generator類型的對象。yield關鍵字是一個標誌,通知蟒蛇編譯器將特殊對待這樣的功能。一旦從其返回某個值,正常函數將終止。但是在編譯器的幫助下,生成器函數可以被認為是可恢復的。也就是說,將恢復執行上下文,並且執行將從上次運行繼續。直到你顯式調用return,這將引發StopIteration異常(也是迭代器協議的一部分),或者到達函數的末尾。我發現了很多關於引用的generator,但是這onefunctional programming perspective最可消化的。

(現在我想談談背後的基本原理generator,並iterator基於我自己的理解。我希望這可以幫助你掌握迭代器和生成器的基本動機。這樣的概念也出現在其他語言中,比如C#。)

據我所知,當我們想要處理大量數據時,我們通常首先將數據存儲在某處,然後逐個處理。但這種直觀的方法存在問題。如果數據量很大,那麼事先將它們作為一個整體存儲起來是很昂貴的。因此,不是data直接存儲自身,為什麼不metadata間接存儲某種,即the logic how the data is computed

有兩種方法來包裝這樣的元數據。

  1. OO方法,我們包裝元數據as a class。這就是所謂的iterator實現迭代器協議(即__next__(),和__iter__()方法)的人。這也是常見的迭代器設計模式
  2. 功能方法,我們包裝元數據as a function。這就是所謂的generator function。但在引擎蓋下,返回的generator object仍然是IS-A迭代器,因為它還實現了迭代器協議。

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


產量為您提供發電機。

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來通過調用堆棧推送,而不是使用某種堆棧變量。生成器也可以用於專門的樹遍歷,以及其他各種方式。


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就好return +=


像每個答案所暗示的那樣,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"

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這種變化提出以允許用戶將數據發送到發電機,就像一個會接受它。要發送數據,必須能夠將其分配給某些內容,為此,語句將無效。


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


另一個TL; DR

列表上的迭代器next()返回列表的下一個元素

迭代器生成器next()將動態計算下一個元素(執行代碼)

您可以看到yield / generator作為一種從外部手動運行控制流的方法(如繼續循環一步),通過調用next,無論流程如何復雜。

注意:生成器不是正常功能。它記住了先前的狀態,如局部變量(堆棧)。有關詳細說明,請參閱其他答案或文章。生成器只能迭代一次。你可以沒有yield,但它不會那麼好,所以它可以被認為是'非常好'的語言糖。


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

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

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

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

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

for item in sequence:

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

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

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


收益率是一個對象

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

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

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

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

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

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


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

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


元類的一個用途是自動向實例添加新屬性和方法。

例如,如果你看看Django模型 ,他們的定義看起來有點令人困惑。 看起來好像只是定義了類屬性:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

但是,在運行時,Person對象充滿了各種有用的方法。 查看source了解一些神奇的元素。





python iterator generator yield coroutine