1. 程式人生 > >python解釋 yield 和 Generators(生成器)

python解釋 yield 和 Generators(生成器)

yield 和 Generators(生成器)

轉自:http://www.oschina.net/translate/improve-your-python-yield-and-generators-explained

原文:http://www.jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/


在開始課程之前,我要求學生們填寫一份調查表,這個調查表反映了它們對 Python 中一些概念的理解情況。一些話題(“if/else 控制流”或者“定義和使用函式”)對於大多數學生是沒有問題的。但是有一些話題,大多數學生只有很少,或者完全沒有任何接觸,尤其是“生成器和 yield 關鍵字”。我猜這對大多數新手 Python 程式設計師也是如此。

有事實表明,在我花了大功夫後,有些人仍然不能理解生成器和 yield 關鍵字。我想讓這個問題有所改善。在這篇文章中,我將解釋 yield 關鍵字到底是什麼,為什麼它是有用的,以及如何來使用它。

注意:最近幾年,生成器的功能變得越來越強大,它已經被加入到了 PEP。在我的下一篇文章中,我會通過協程(coroutine),協同式多工處理(cooperative multitasking),以及非同步 IO(asynchronous I/O)(尤其是 GvR 正在研究的“tulip”原型的實現)來介紹 yield 的真正威力。但是在此之前,我們要對生成器和 yield 有一個紮實的理解。




協程(協同程式)與子例程

我們呼叫一個普通的 Python 函式時,一般是從函式的第一行程式碼開始執行,結束於 return 語句、異常或者函式結束(可以看作隱式的返回 None)。一旦函式將控制權交還給呼叫者,就意味著全部結束。函式中做的所有工作以及儲存在區域性變數中的資料都將丟失。再次呼叫這個函式時,一切都將從頭建立。

對於在計算機程式設計中所討論的函式,這是很標準的流程。這樣的函式只能返回一個值,不過,有時可以建立能產生一個序列的函式還是有幫助的。要做到這一點,這種函式需要能夠“儲存自己的工作”。

我說過,能夠“產生一個序列”是因為我們的函式並沒有像通常意義那樣返回。return 隱含的意思是函式正將執行程式碼的控制權返回給函式被呼叫的地方。而 yield 的隱含意思是控制權的轉移是臨時和自願的,我們的函式將來還會收回控制權。

在 Python 中,擁有這種能力的“函式”被稱為生成器,它非常的有用。生成器(以及 yield 語句)最初的引入是為了讓程式設計師可以更簡單的編寫用來產生值的序列的程式碼。 以前,要實現類似隨機數生成器的東西,需要實現一個類或者一個模組,在生成資料的同時保持對每次呼叫之間狀態的跟蹤。引入生成器之後,這變得非常簡單。

為了更好的理解生成器所解決的問題,讓我們來看一個例子。在瞭解這個例子的過程中,請始終記住我們需要解決的問題:生成值的序列


注意:在 Python 之外,最簡單的生成器應該是被稱為協程(coroutines)的東西。在本文中,我將使用這個術語。請記住,在 Python 的概念中,這裡提到的協程就是生成器。Python 正式的術語是生成器;協程只是便於討論,在語言層面並沒有正式定義。


例子:有趣的素數

假設你的老闆讓你寫一個函式,輸入引數是一個 int 的 list,返回一個可以迭代的包含素數 1 的結果。

記住,迭代器(Iterable) 只是物件每次返回特定成員的一種能力。

你肯定認為"這很簡單",然後很快寫出下面的程式碼:

  1. def get_primes(input_list):
  2.     result_list = list()
  3.     for element in input_list:
  4.         if is_prime(element):
  5.             result_list.append()
  6.     return result_list
  7. # 或者更好一些的...
  8. def get_primes(input_list):
  9.     return (element for element in input_list if is_prime(element))
  10. # 下面是 is_prime 的一種實現...
  11. def is_prime(number):
  12.     if number > 1:
  13.         if number == 2:
  14.             return True
  15.         if number % 2 == 0:
  16.             return False
  17.         for current in range(3, int(math.sqrt(number) + 1), 2):
  18.             if number % current == 0:
  19.                 return False
  20.         return True
  21.     return False
複製程式碼


上面 is_prime 的實現完全滿足了需求,所以我們告訴老闆已經搞定了。她反饋說我們的函式工作正常,正是她想要的。


處理無限序列

噢,真是如此嗎?過了幾天,老闆過來告訴我們她遇到了一些小問題:她打算把我們的 get_primes 函式用於一個很大的包含數字的 list。實際上,這個 list 非常大,僅僅是建立這個 list 就會用完系統的所有記憶體。為此,她希望能夠在呼叫 get_primes 函式時帶上一個 start 引數,返回所有大於這個引數的素數(也許她要解決 Project Euler problem 10)。

我們來看看這個新需求,很明顯只是簡單的修改 get_primes 是不可能的。 自然,我們不可能返回包含從 start 到無窮的所有的素數的列表(雖然有很多有用的應用程式可以用來操作無限序列)。看上去用普通函式處理這個問題的可能性比較渺茫。

在我們放棄之前,讓我們確定一下最核心的障礙,是什麼阻止我們編寫滿足老闆新需求的函式。通過思考,我們得到這樣的結論:函式只有一次返回結果的機會,因而必須一次返回所有的結果。得出這樣的結論似乎毫無意義;“函式不就是這樣工作的麼”,通常我們都這麼認為的。可是,不學不成,不問不知,“如果它們並非如此呢?”

想象一下,如果 get_primes 可以只是簡單返回下一個值,而不是一次返回全部的值,我們能做什麼?我們就不再需要建立列表。沒有列表,就沒有記憶體的問題。由於老闆告訴我們的是,她只需要遍歷結果,她不會知道我們實現上的區別。

不幸的是,這樣做看上去似乎不太可能。即使是我們有神奇的函式,可以讓我們從 n 遍歷到無限大,我們也會在返回第一個值之後卡住:

  1. def get_primes(start):
  2.     for element in magical_infinite_range(start):
  3.         if is_prime(element):
  4.             return element
複製程式碼


假設這樣去呼叫 get_primes:

  1. def solve_number_10():
  2.     # She *is* working on Project Euler #10, I knew it!
  3.     total = 2
  4.     for next_prime in get_primes(3):
  5.         if next_prime < 2000000:
  6.             total += next_prime
  7.         else:
  8.             print(total)
  9.             return
複製程式碼


顯然,在 get_primes 中,一上來就會碰到輸入等於 3 的,並且在函式的第 4 行返回。與直接返回不同,我們需要的是在退出時可以為下一次請求準備一個值。

不過函式做不到這一點。當函式返回時,意味著全部完成。我們保證函式可以再次被呼叫,但是我們沒法保證說,“呃,這次從上次退出時的第 4 行開始執行,而不是常規的從第一行開始”。函式只有一個單一的入口:函式的第 1 行程式碼。


走進生成器

這類問題極其常見以至於 Python 專門加入了一個結構來解決它:生成器。一個生成器會“生成”值。建立一個生成器幾乎和生成器函式的原理一樣簡單。

一個生成器函式的定義很像一個普通的函式,除了當它要生成一個值的時候,使用 yield 關鍵字而不是 return。如果一個 def 的主體包含 yield,這個函式會自動變成一個生成器(即使它包含一個 return)。除了以上內容,建立一個生成器沒有什麼多餘步驟了。

生成器函式返回生成器的迭代器。這可能是你最後一次見到“生成器的迭代器”這個術語了, 因為它們通常就被稱作“生成器”。要注意的是生成器就是一類特殊的迭代器。作為一個迭代器,生成器必須要定義一些方法(method),其中一個就是 __next__()。如同迭代器一樣,我們可以使用 next() 函式來獲取下一個值。

為了從生成器獲取下一個值,我們使用 next() 函式,就像對付迭代器一樣(next() 會操心如何呼叫生成器的 __next__() 方法,不用你操心)。既然生成器是一個迭代器,它可以被用在 for 迴圈中。

每當生成器被呼叫的時候,它會返回一個值給呼叫者。在生成器內部使用 yield 來完成這個動作(例如 yield 7)。為了記住 yield 到底幹了什麼,最簡單的方法是把它當作專門給生成器函式用的特殊的 return(加上點小魔法)。


下面是一個簡單的生成器函式:

  1. >>> def simple_generator_function():
  2. >>>    yield 1
  3. >>>    yield 2
  4. >>>    yield 3
複製程式碼


這裡有兩個簡單的方法來使用它:

  1. >>> for value in simple_generator_function():
  2. >>>     print(value)
  3. 1
  4. 2
  5. 3
  6. >>> our_generator = simple_generator_function()
  7. >>> next(our_generator)
  8. 1
  9. >>> next(our_generator)
  10. 2
  11. >>> next(our_generator)
  12. 3
複製程式碼



魔法?

那麼神奇的部分在哪裡?

我很高興你問了這個問題!當一個生成器函式呼叫 yield,生成器函式的“狀態”會被凍結,所有的變數的值會被保留下來,下一行要執行的程式碼的位置也會被記錄,直到再次呼叫 next()。一旦 next() 再次被呼叫,生成器函式會從它上次離開的地方開始。如果永遠不呼叫 next(),yield 儲存的狀態就被無視了。

我們來重寫 get_primes() 函式,這次我們把它寫作一個生成器。注意我們不再需要 magical_infinite_range 函數了。使用一個簡單的 while 迴圈,我們創造了自己的無窮串列。

  1. def get_primes(number):
  2.     while True:
  3.         if is_prime(number):
  4.             yield number
  5.         number += 1
複製程式碼


如果生成器函式呼叫了 return,或者執行到函式的末尾,會出現一個 StopIteration 異常。 這會通知 next() 的呼叫者這個生成器沒有下一個值了(這就是普通迭代器的行為)。這也是這個 while 迴圈在我們的 get_primes() 函數出現的原因。如果沒有這個 while,當我們第二次呼叫 next() 的時候,生成器函式會執行到函式末尾,觸發 StopIteration 異常。一旦生成器的值用完了,再呼叫 next() 就會出現錯誤,所以你只能將每個生成器的使用一次。下面的程式碼是錯誤的:

  1. >>> our_generator = simple_generator_function()
  2. >>> for value in our_generator:
  3. >>>     print(value)
  4. >>> # 我們的生成器沒有下一個值了...
  5. >>> print(next(our_generator))
  6. Traceback (most recent call last):
  7.   File "<ipython-input-13-7e48a609051a>", line 1, in <module>
  8.     next(our_generator)
  9. StopIteration
  10. >>> # 然而,我們總可以再建立一個生成器
  11. >>> # 只需再次呼叫生成器函式即可
  12. >>> new_generator = simple_generator_function()
  13. >>> print(next(new_generator)) # 工作正常
  14. 1
複製程式碼


因此,這個 while 迴圈是用來確保生成器函式永遠也不會執行到函式末尾的。只要呼叫 next() 這個生成器就會生成一個值。這是一個處理無窮序列的常見方法(這類生成器也是很常見的)。


執行流程

讓我們回到呼叫 get_primes 的地方:solve_number_10。

  1. def solve_number_10():
  2.     # She *is* working on Project Euler #10, I knew it!
  3.     total = 2
  4.     for next_prime in get_primes(3):
  5.         if next_prime < 2000000:
  6.             total += next_prime
  7.         else:
  8.             print(total)
  9.             return
複製程式碼


我們來看一下 solve_number_10 的 for 迴圈中對 get_primes 的呼叫,觀察一下前幾個元素是如何建立的有助於我們的理解。當 for 迴圈從 get_primes 請求第一個值時,我們進入 get_primes,這時與進入普通函式沒有區別。

  • 進入第三行的 while 迴圈
  • 停在 if 條件判斷(3 是素數)
  • 通過 yield 將 3 和執行控制權返回給 solve_number_10


接下來,回到 insolve_number_10:

  • for 迴圈得到返回值 3
  • for 迴圈將其賦給 next_prime
  • total 加上 next_prime
  • for 迴圈從 get_primes 請求下一個值


這次,進入 get_primes 時並沒有從開頭執行,我們從第 5 行繼續執行,也就是上次離開的地方。

  1. def get_primes(number):
  2.     while True:
  3.         if is_prime(number):
  4.             yield number
  5.         number += 1 # <<<<<<<<<<
複製程式碼


最關鍵的是,number 還保持我們上次呼叫 yield 時的值(例如 3)。記住,yield 會將值傳給 next() 的呼叫方,同時還會儲存生成器函式的“狀態”。接下來,number 加到 4,回到 while 迴圈的開始處,然後繼續增加直到得到下一個素數(5)。我們再一次把 number 的值通過 yield 返回給 solve_number_10 的 for 迴圈。這個週期會一直執行,直到 for 迴圈結束(得到的素數大於2,000,000)。


更給力點

在 PEP 342 中加入了將值傳給生成器的支援。PEP 342 加入了新的特性,能讓生成器在單一語句中實現,生成一個值(像從前一樣),接受一個值,或同時生成一個值並接受一個值。

我們用前面那個關於素數的函式來展示如何將一個值傳給生成器。這一次,我們不再簡單地生成比某個數大的素數,而是找出比某個數的等比級數大的最小素數(例如 10, 我們要生成比 10,100,1000,10000 ... 大的最小素數)。我們從 get_primes 開始:

  1. def print_successive_primes(iterations, base=10):
  2.     # 像普通函式一樣,生成器函式可以接受一個引數
  3.    
  4.     prime_generator = get_primes(base)
  5.     # 這裡以後要加上點什麼
  6.     for power in range(iterations):
  7.         # 這裡以後要加上點什麼
  8. def get_primes(number):
  9.     while True:
  10.         if is_prime(number):
  11.         # 這裡怎麼寫?
複製程式碼


get_primes 的後幾行需要著重解釋。yield 關鍵字返回 number 的值,而像 other = yield foo 這樣的語句的意思是,“返回 foo 的值,這個值返回給呼叫者的同時,將 other 的值也設定為那個值”。你可以通過 send 方法來將一個值“傳送”給生成器。

  1. def get_primes(number):
  2.     while True:
  3.         if is_prime(number):
  4.             number = yield number
  5.         number += 1
複製程式碼


通過這種方式,我們可以在每次執行 yield 的時候為 number 設定不同的值。現在我們可以補齊 print_successive_primes 中缺少的那部分程式碼:

  1. def print_successive_primes(iterations, base=10):
  2.     prime_generator = get_primes(base)
  3.     prime_generator.send(None)
  4.     for power in range(iterations):
  5.         print(prime_generator.send(base ** power))
複製程式碼


這裡有兩點需要注意:首先,我們列印的是 generator.send 的結果,這是沒問題的,因為 send 在傳送資料給生成器的同時還返回生成器通過 yield 生成的值(就如同生成器中 yield 語句做的那樣)。

第二點,看一下 prime_generator.send(None) 這一行,當你用 send 來“啟動”一個生成器時(就是從生成器函式的第一行程式碼執行到第一個 yield 語句的位置),你必須傳送 None。這不難理解,根據剛才的描述,生成器還沒有走到第一個 yield 語句,如果我們發生一個真實的值,這時是沒有人去“接收”它的。一旦生成器啟動了,我們就可以像上面那樣傳送資料了。


綜述

在本系列文章的後半部分,我們將討論一些 yield 的高階用法及其效果。yield 已經成為 Python 最強大的關鍵字之一。現在我們已經對 yield 是如何工作的有了充分的理解,我們已經有了必要的知識,可以去了解 yield 的一些更“費解”的應用場景。

不管你信不信,我們其實只是揭開了 yield 強大能力的一角。例如,send 確實如前面說的那樣工作,但是在像我們的例子這樣,只是生成簡單的序列的場景下,send 幾乎從來不會被用到。下面我貼一段程式碼,展示 send 通常的使用方式。對於這段程式碼如何工作以及為何可以這樣工作,在此我並不打算多說,它將作為第二部分很不錯的熱身。

  1. import random
  2. def get_data():
  3.     """返回0到9之間的3個隨機數"""
  4.     return random.sample(range(10), 3)
  5. def consume():
  6.     """顯示每次傳入的整數列表的動態平均值"""
  7.     running_sum = 0
  8.     data_items_seen = 0
  9.     while True:
  10.         data = yield
  11.         data_items_seen += len(data)
  12.         running_sum += sum(data)
  13.         print('The running average is {}'.format(running_sum / float(data_items_seen)))
  14. def produce(consumer):
  15.     """產生序列集合,傳遞給消費函式(consumer)"""
  16.     while True:
  17.         data = get_data()
  18.         print('Produced {}'.format(data))
  19.         consumer.send(data)
  20.         yield
  21. if __name__ == '__main__':
  22.     consumer = consume()
  23.     consumer.send(None)
  24.     producer = produce(consumer)
  25.     for _ in range(10):
  26.         print('Producing...')
  27.         next(producer)
複製程式碼



請謹記……

我希望您可以從本文的討論中獲得一些關鍵的思想:

  • generator 是用來產生一系列值的
  • yield 則像是 generator 函式的返回結果
  • yield 唯一所做的另一件事就是儲存一個 generator 函式的狀態
  • generator 就是一個特殊型別的迭代器(iterator)
  • 和迭代器相似,我們可以通過使用 next() 來從 generator 中獲取下一個值
  • 通過隱式地呼叫 next() 來忽略一些值

轉自:http://www.oschina.net/translate/improve-your-python-yield-and-generators-explained

原文:http://www.jeffknupp.com/blog/2013/04/07/improve-your-python-yield-and-generators-explained/


在開始課程之前,我要求學生們填寫一份調查表,這個調查表反映了它們對 Python 中一些概念的理解情況。一些話題(“if/else 控制流或者定義和使用函式)對於大多數學生是沒有問題的。但是有一些話題,大多數學生只有很少,或者完全沒有任何接觸,尤其是生成器和 yield 關鍵字。我猜這對大多數新手 Python 程式設計師也是如此。

有事實表明,在我花了大功夫後,有些人仍然不能理解生成器和 yield 關鍵字。我想讓這個問題有所改善。在這篇文章中,我將解釋 yield 關鍵字到底是什麼,為什麼它是有用的,以及如何來使用它。

注意:最近幾年,生成器的功能變得越來越強大,它已經被加入到了 PEP。在我的下一篇文章中,我會通過協程(coroutine),協同式多工處理(cooperative multitasking),以及非同步 IOasynchronous I/O)(尤其是 GvR 正在研究的“tulip”原型的實現)來介紹 yield 的真正威力。但是在此之前,我們要對生成器和 yield 有一個紮實的理解。


協程(協同程式)與子例程

我們呼叫一個普通的 Python 函式時,一般是從函式的第一行程式碼開始執行,結束於 return 語句、異常或者函式結束(可以看作隱式的返回 None)。一旦函式將控制權交還給呼叫者,就意味著全部結束。函式中做的所有工作以及儲存在區域性變數中的資料都將丟失。再次呼叫這個函式時,一切都將從頭建立。

對於在計算機程式設計中所討論的函式,這是很標準的流程。這樣的函式只能返回一個值,不過,有時可以建立能產生一個序列的函式還是有幫助的。要做到這一點,這種函式需要能夠儲存自己的工作

我說過,能夠產生一個序列是因為我們的函式並沒有像通常意義那樣返回。return 隱含的意思是函式正將執行程式碼的控制權返回給函式被呼叫的地方。而 yield 的隱含意思是控制權的轉移是臨時和自願的,我們的函式將來還會收回控制權。

Python 中,擁有這種能力的函式被稱為生成器,它非常的有用。生成器(以及 yield 語句)最初的引入是為了讓程式設計師可以更簡單的編寫用來產生值的序列的程式碼。 以前,要實現類似隨機數生成器的東西,需要實現一個類或者一個模組,在生成資料的同時保持對每次呼叫之間狀態的跟蹤。引入生成器之後,這變得非常簡單。

為了更好的理解生成器所解決的問題,讓我們來看一個例子。在瞭解這個例子的過程中,請始終記住我們需要解決的問題:生成值的序列

注意:在 Python 之外,最簡單的生成器應該是被稱為協程(coroutines)的東西。在本文中,我將使用這個術語。請記住,在 Python 的概念中,這裡提到的協程就是生成器。Python 正式的術語是生成器;協程只是便於討論,在語言層面並沒有正式定義。


例子:有趣的素數

假設你的老闆讓你寫一個函式,輸入引數是一個 int list,返回一個可以迭代的包含素數 1 的結果。

記住,迭代器(Iterable) 只是物件每次返回特定成員的一種能力。

你肯定認為"這很簡單",然後很快寫出下面的程式碼:

1.   def get_primes(input_list):

2.       result_list = list()

3.       for element in input_list:

4.           if is_prime(element):

5.               result_list.append()

6.    

7.       return result_list

8.    

9.   # 或者更好一些的...

10. 

11.def get_primes(input_list):

12.    return (element for element in input_list if is_prime(element))

13. 

14.# 下面是 is_prime 的一種實現...

15. 

16.def is_prime(number):

17.    if number > 1:

18.        if number == 2:

19.            return True

20.        if number % 2 == 0:

21.            return False

22.        for current in range(3, int(math.sqrt(number) + 1), 2):

23.            if number % current == 0:

24.                return False

25.        return True

26.    return False

複製程式碼


上面 is_prime 的實現完全滿足了需求,所以我們告訴老闆已經搞定了。她反饋說我們的函式工作正常,正是她想要的。


處理無限序列

噢,真是如此嗎?過了幾天,老闆過來告訴我們她遇到了一些小問題:她打算把我們的 get_primes 函式用於一個很大的包含數字的 list。實際上,這個 list 非常大,僅僅是建立這個 list 就會用完系統的所有記憶體。為此,她希望能夠在呼叫 get_primes 函式時帶上一個 start 引數,返回所有大於這個引數的素數(也許她要解決 Project Euler problem 10)。

我們來看看這個新需求,很明顯只是簡單的修改 get_primes 是不可能的。 自然,我們不可能返回包含從 start 到無窮的所有的素數的列表(雖然有很多有用的應用程式可以用來操作無限序列)。看上去用普通函式處理這個問題的可能性比較渺茫。

在我們放棄之前,讓我們確定一下最核心的障礙,是什麼阻止我們編寫滿足老闆新需求的函式。通過思考,我們得到這樣的結論:函式只有一次返回結果的機會,因而必須一次返回所有的結果。得出這樣的結論似乎毫無意義;函式不就是這樣工作的麼,通常我們都這麼認為的。可是,不學不成,不問不知,如果它們並非如此呢?

想象一下,如果 get_primes 可以只是簡單返回下一個值,而不是一次返回全部的值,我們能做什麼?我們就不再需要建立列表。沒有列表,就沒有記憶體的問題。由於老闆告訴我們的是,她只需要遍歷結果,她不會知道我們實現上的區別。

不幸的是,這樣做看上去似乎不太可能。即使是我們有神奇的函式,可以讓我們從 n 遍歷到無限大,我們也會在返回第一個值之後卡住:

1.   def get_primes(start):

2.       for element in magical_infinite_range(start):

3.           if is_prime(element):

4.               return element

複製程式碼


假設這樣去呼叫 get_primes

1.   def solve_number_10():

2.       # She *is* working on Project Euler #10, I knew it!

3.       total = 2

4.       for next_prime in get_primes(3):

5.           if next_prime < 2000000:

6.               total += next_prime

7.           else:

8.               print(total)

9.               return

複製程式碼


顯然,在 get_primes 中,一上來就會碰到輸入等於 3 的,並且在函式的第 4 行返回。與直接返回不同,我們需要的是在退出時可以為下一次請求準備一個值。

不過函式做不到這一點。當函式返回時,意味著全部完成。我們保證函式可以再次被呼叫,但是我們沒法保證說,呃,這次從上次退出時的第 4 行開始執行,而不是常規的從第一行開始。函式只有一個單一的入口:函式的第 1 行程式碼。


走進生成器

這類問題極其常見以至於 Python 專門加入了一個結構來解決它:生成器。一個生成器會生成值。建立一個生成器幾乎和生成器函式的原理一樣簡單。

一個生成器函式的定義很像一個普通的函式,除了當它要生成一個值的時候,使用 yield 關鍵字而不是 return。如果一個 def 的主體包含 yield,這個函式會自動變成一個生成器(即使它包含一個 return)。除了以上內容,建立一個生成器沒有什麼多餘步驟了。

生成器函式返回生成器的迭代器。這可能是你最後一次見到生成器的迭代器這個術語了, 因為它們通常就被稱作生成器。要注意的是生成器就是一類特殊的迭代器。作為一個迭代器,生成器必須要定義一些方法(method),其中一個就是 __next__()。如同迭代器一樣,我們可以使用 next() 函式來獲取下一個值。

為了從生成器獲取下一個值,我們使用 next() 函式,就像對付迭代器一樣(next() 會操心如何呼叫生成器的 __next__() 方法,不用你操心)。既然生成器是一個迭代器,它可以被用在 for 迴圈中。

每當生成器被呼叫的時候,它會返回一個值給呼叫者。在生成器內部使用 yield 來完成這個動作(例如 yield 7)。為了記住 yield 到底幹了什麼,最簡單的方法是把它當作專門給生成器函式用的特殊的 return(加上點小魔法)。


下面是一個簡單的生成器函式:

1.   >>> def simple_generator_function():

2.   >>>    yield 1

3.   >>>    yield 2

4.   >>>    yield 3

複製程式碼


這裡有兩個簡單的方法來使用它:

1.   >>> for value in simple_generator_function():

2.   >>>     print(value)

3.   1

4.   2

5.   3

6.   >>> our_generator = simple_generator_function()

7.   >>> next(our_generator)

8.   1

9.   >>> next(our_generator)

10.2

11.>>> next(our_generator)

12.3

複製程式碼



魔法?

那麼神奇的部分在哪裡?

我很高興你問了這個問題!當一個生成器函式呼叫 yield,生成器函式的狀態會被凍結,所有的變數的值會被保留下來,下一行要執行的程式碼的位置也會被記錄,直到再次呼叫 next()。一旦 next() 再次被呼叫,生成器函式會從它上次離開的地方開始。如果永遠不呼叫 next()yield 儲存的狀態就被無視了。

我們來重寫 get_primes() 函式,這次我們把它寫作一個生成器。注意我們不再需要 magical_infinite_range 函數了。使用一個簡單的 while 迴圈,我們創造了自己的無窮串列。

1.   def get_primes(number):

2.       while True:

3.           if is_prime(number):

4.               yield number

5.           number += 1

複製程式碼


如果生成器函式呼叫了 return,或者執行到函式的末尾,會出現一個 StopIteration 異常。 這會通知 next() 的呼叫者這個生成器沒有下一個值了(這就是普通迭代器的行為)。這也是這個 while 迴圈在我們的 get_primes() 函數出現的原因。如果沒有這個 while,當我們第二次呼叫 next() 的時候,生成器函式會執行到函式末尾,觸發 StopIteration 異常。一旦生成器的值用完了,再呼叫 next() 就會出現錯誤,所以你只能將每個生成器的使用一次。下面的程式碼是錯誤的:

1.   >>> our_generator = simple_generator_function()

2.   >>> for value in our_generator:

3.   >>>     print(value)

4.    

5.   >>> # 我們的生成器沒有下一個值了...

6.   >>> print(next(our_generator))

7.   Traceback (most recent call last):

8.     File "<ipython-input-13-7e48a609051a>", line 1, in <module>

9.       next(our_generator)

10.StopIteration

11. 

12.>>> # 然而,我們總可以再建立一個生成器

13.>>> # 只需再次呼叫生成器函式即可

14. 

15.>>> new_generator = simple_generator_function()

16.>>> print(next(new_generator)) # 工作正常

17.1

複製程式碼


因此,這個 while 迴圈是用來確保生成器函式永遠也不會執行到函式末尾的。只要呼叫 next() 這個生成器就會生成一個值。這是一個處理無窮序列的常見方法(這類生成器也是很常見的)。


執行流程

讓我們回到呼叫 get_primes 的地方:solve_number_10

1.   def solve_number_10():

2.       # She *is* working on Project Euler #10, I knew it!

3.       total = 2

4.       for next_prime in get_primes(3):

5.           if next_prime < 2000000:

6.               total += next_prime

7.           else:

8.               print(total)

9.               return

複製程式碼


我們來看一下 solve_number_10 for 迴圈中對 get_primes 的呼叫,觀察一下前幾個元素是如何建立的有助於我們的理解。當 for 迴圈從 get_primes 請求第一個值時,我們進入 get_primes,這時與進入普通函式沒有區別。

1.   進入第三行的 while 迴圈

2.   停在 if 條件判斷(3 是素數)

3.   通過 yield 3 和執行控制權返回給 solve_number_10


接下來,回到 insolve_number_10

1.   for 迴圈得到返回值 3

2.   for 迴圈將其賦給 next_prime

3.   total 加上 next_prime

4.   for 迴圈從 get_primes 請求下一個值


這次,進入 get_primes 時並沒有從開頭執行,我們從第 5 行繼續執行,也就是上次離開的地方。

1.   def get_primes(number):

2.       while True:

3.           if is_prime(number):

4.               yield number

5.           number += 1 # <<<<<<<<<<

複製程式碼


最關鍵的是,number 還保持我們上次呼叫 yield 時的值(例如 3)。記住,yield 會將值傳給 next() 的呼叫方,同時還會儲存生成器函式的狀態。接下來,number 加到 4,回到 while 迴圈的開始處,然後繼續增加直到得到下一個素數(5)。我們再一次把 number 的值通過 yield 返回給 solve_number_10 for 迴圈。這個週期會一直執行,直到 for 迴圈結束(得到的素數大於2,000,000)。


更給力點

PEP 342 中加入了將值傳給生成器的支援。PEP 342 加入了新的特性,能讓生成器在單一語句中實現,生成一個值(像從前一樣),接受一個值,或同時生成一個值並接受一個值。

我們用前面那個關於素數的函式來展示如何將一個值傳給生成器。這一次,我們不再簡單地生成比某個數大的素數,而是找出比某個數的等比級數大的最小素數(例如 10, 我們要生成比 10100100010000 ... 大的最小素數)。我們從 get_primes 開始:

1.   def print_successive_primes(iterations, base=10):

2.       # 像普通函式一樣,生成器函式可以接受一個引數

3.      

4.       prime_generator = get_primes(base)

5.       # 這裡以後要加上點什麼

6.       for power in range(iterations):

7.           # 這裡以後要加上點什麼

8.    

9.   def get_primes(number):

10.    while True:

11.        if is_prime(number):

12.        # 這裡怎麼寫?

複製程式碼


get_primes
的後幾行需要著重解釋。yield 關鍵字返回 number 的值,而像 other = yield foo 這樣的語句的意思是,返回 foo 的值,這個值返回給呼叫者的同時,將 other 的值也設定為那個值。你可以通過 send 方法來將一個值傳送