深刻解釋yield和Generators

生成器和yield關鍵字多是Python裏面最強大的最難理解的概念之一(或許沒有之一), 可是並不妨礙yield成爲Python裏面最強大的關鍵字,對於初學者來說確實很是難於理解,來看一篇關於yield的國外大牛寫的文章,讓你快速理解yield。 文章有點長,請耐心讀完, 過程當中有些例子, 按部就班,讓你不以爲枯燥。python

生成器

生成器是經過一個或多個yield表達式構成的函數,每個生成器都是一個迭代器(可是迭代器不必定是生成器)。程序員

若是一個函數包含yield關鍵字,這個函數就會變爲一個生成器。編程

生成器並不會一次返回全部結果,而是每次遇到yield關鍵字後返回相應結果,並保留函數當前的運行狀態,等待下一次的調用。bash

因爲生成器也是一個迭代器,那麼它就應該支持next方法來獲取下一個值。(也可使用.__next__()屬性, 在python2 中是.next())app

協程與子例程

咱們調用一個普通的Python函數時,通常是從函數的第一行代碼開始執行,結束於return語句、異常或者函數結束(能夠看做隱式的返回None)。一旦函數將控制權交還給調用者,就意味着所有結束。函數中作的全部工做以及保存在局部變量中的數據都將丟失。再次調用這個函數時,一切都將從頭建立。 函數

 

對於在計算機編程中所討論的函數,這是很標準的流程。這樣的函數只能返回一個值,不過,有時能夠建立能產生一個序列的函數仍是有幫助的。要作到這一點,這種函數須要可以「保存本身的工做」。 spa

 

我說過,可以「產生一個序列」是由於咱們的函數並無像一般意義那樣返回。return隱含的意思是函數正將執行代碼的控制權返回給函數被調用的地方。而"yield"的隱含意思是控制權的轉移是臨時和自願的,咱們的函數未來還會收回控制權。code

在Python中,擁有這種能力的「函數」被稱爲生成器,它很是的有用。生成器(以及yield語句)最初的引入是爲了讓程序員能夠更簡單的編寫用來產生值的序列的代碼。 之前,要實現相似隨機數生成器的東西,須要實現一個類或者一個模塊,在生成數據的同時保持對每次調用之間狀態的跟蹤。引入生成器以後,這變得很是簡單。協程

 

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

 

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

 

例子:有趣的素數

 

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

 

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

 

你確定認爲"這很簡單",而後很快寫出下面的代碼:

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

上面 is_prime 的實現徹底知足了需求,因此咱們告訴老闆已經搞定了。她反饋說咱們的函數工做正常,正是她想要的。

處理無限序列

噢,真是如此嗎?過了幾天,老闆過來告訴咱們她遇到了一些小問題:她打算把咱們的get_primes函數用於一個很大的包含數字的list。實際上,這個list很是大,僅僅是建立這個list就會用完系統的全部內存。爲此,她但願可以在調用get_primes函數時帶上一個start參數,返回全部大於這個參數的素數(也許她要解決 Project Euler problem 10)。

 

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

 

在咱們放棄以前,讓咱們肯定一下最核心的障礙,是什麼阻止咱們編寫知足老闆新需求的函數。經過思考,咱們獲得這樣的結論:函數只有一次返回結果的機會,於是必須一次返回全部的結果。得出這樣的結論彷佛毫無心義;「函數不就是這樣工做的麼」,一般咱們都這麼認爲的。但是,不學不成,不問不知,「若是它們並不是如此呢?」

 

想象一下,若是get_primes能夠只是簡單返回下一個值,而不是一次返回所有的值,咱們能作什麼?咱們就再也不須要建立列表。沒有列表,就沒有內存的問題。因爲老闆告訴咱們的是,她只須要遍歷結果,她不會知道咱們實現上的區別。

 

不幸的是,這樣作看上去彷佛不太可能。即便是咱們有神奇的函數,可讓咱們從n遍歷到無限大,咱們也會在返回第一個值以後卡住:

1
2
3
4
def  get_primes(start):
     for  element  in  magical_infinite_range(start):
         if  is_prime(element):
             return  element

假設這樣去調用get_primes:

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

顯然,在get_primes中,一上來就會碰到輸入等於3的,而且在函數的第4行返回。與直接返回不一樣,咱們須要的是在退出時能夠爲下一次請求準備一個值。

 

不過函數作不到這一點。當函數返回時,意味着所有完成。咱們保證函數能夠再次被調用,可是咱們無法保證說,「呃,此次從上次退出時的第4行開始執行,而不是常規的從第一行開始」。函數只有一個單一的入口:函數的第1行代碼。

 

走進生成器

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

 

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

 

生成器函數返回生成器的迭代器。這多是你最後一次見到「生成器的迭代器」這個術語了, 由於它們一般就被稱做「生成器」。要注意的是生成器就是一類特殊的迭代器。做爲一個迭代器,生成器必需要定義一些方法(method),其中一個就是__next__()【注意: 在python2中是: next() 方法】。如同迭代器同樣,咱們可使用next()函數來獲取下一個值。

 

爲了從生成器獲取下一個值,咱們使用next()函數,就像對付迭代器同樣。

(next()會操心如何調用生成器的__next__()方法)。既然生成器是一個迭代器,它能夠被用在for循環中。

 

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

 

yield就是專門給生成器用的return(加上點小魔法)。

 

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

1
2
3
4
>>> def simple_generator_function():
>>>    yield 1
>>>    yield 2
>>>    yield 3

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

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

魔法?

那麼神奇的部分在哪裏?我很高興你問了這個問題!當一個生成器函數調用yield,生成器函數的「狀態」會被凍結,全部的變量的值會被保留下來,下一行要執行的代碼的位置也會被記錄,直到再次調用next()。一旦next()再次被調用,生成器函數會從它上次離開的地方開始。若是永遠不調用next(),yield保存的狀態就被無視了。

 

咱們來重寫get_primes()函數,此次咱們把它寫做一個生成器。注意咱們再也不須要magical_infinite_range函數了。使用一個簡單的while循環,咱們創造了本身的無窮串列。

1
2
3
4
5
def  get_primes(number):
     while  True :
         if  is_prime(number):
             yield  number
         number  + =  1

若是生成器函數調用了return,或者執行到函數的末尾,會出現一個StopIteration異常。 這會通知next()的調用者這個生成器沒有下一個值了(這就是普通迭代器的行爲)。這也是這個while循環在咱們的get_primes()函數出現的緣由。若是沒有這個while,當咱們第二次調用next()的時候,生成器函數會執行到函數末尾,觸發StopIteration異常。一旦生成器的值用完了,再調用next()就會出現錯誤,因此你只能將每一個生成器的使用一次。下面的代碼是錯誤的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> our_generator = simple_generator_function()
>>>  for  value  in  our_generator:
>>>     print(value)
>>>  # 咱們的生成器沒有下一個值了...
>>> print(next(our_generator))
Traceback (most recent call last):
   File  "<ipython-input-13-7e48a609051a>" , line 1,  in  <module>
     next(our_generator)
StopIteration
>>>  # 然而,咱們總能夠再建立一個生成器
>>>  # 只需再次調用生成器函數便可
>>> new_generator = simple_generator_function()
>>> print(next(new_generator))  # 工做正常
1

所以,這個while循環是用來確保生成器函數永遠也不會執行到函數末尾的。只要調用next()這個生成器就會生成一個值。這是一個處理無窮序列的常見方法(這類生成器也是很常見的)。

執行流程

讓咱們回到調用get_primes的地方:solve_number_10。

1
2
3
4
5
6
7
8
9
def  solve_number_10():
     # She *is* working on Project Euler #10, I knew it!
     total  =  2
     for  next_prime  in  get_primes( 3 ):
         if  next_prime <  2000000 :
             total  + =  next_prime
         else :
             print (total)
             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
2
3
4
5
def  get_primes(number):
     while  True :
         if  is_prime(number):
             yield  number
         number  + =  1  # <<<<<<<<<<

最關鍵的是,number還保持咱們上次調用yield時的值(例如3)。記住,yield會將值傳給next()的調用方,同時還會保存生成器函數的「狀態」。接下來,number加到4,回到while循環的開始處,而後繼續增長直到獲得下一個素數(5)。咱們再一次把number的值經過yield返回給solve_number_10的for循環。這個週期會一直執行,直到for循環結束(獲得的素數大於2,000,000)。

相關文章
相關標籤/搜索