在開始課程之前,我要求學生們填寫一份調查表,這個調查表反映了它們對Python中一些概念的理解情況。一些話題("if/else控制流" 或者 "定義和使用函數")對于大多數學生是沒有問題的。但是有一些話題,大多數學生只有很少,或者完全沒有任何接觸,尤其是“生成器和yield關鍵字”。我猜這對大多數新手Python程序員也是如此。
有事實表明,在我花了大功夫后,有些人仍然不能理解生成器和yield關鍵字。我想讓這個問題有所改善。在這篇文章中,我將解釋yield關鍵字到底是什么,為什么它是有用的,以及如何來使用它。
注意:最近幾年,生成器的功能變得越來越強大,它已經被加入到了PEP。在我的下一篇文章中,我會通過協程(coroutine),協同式多任務處理(cooperative multitasking),以及異步IO(asynchronous I/O)(尤其是GvR正在研究的 "tulip" 原型的實現)來介紹yield的真正威力。但是在此之前,我們要對生成器和yield有一個扎實的理解.
協程與子例程
我們調用一個普通的Python函數時,一般是從函數的第一行代碼開始執行,結束于return語句、異?;蛘吆瘮到Y束(可以看作隱式的返回None)。一旦函數將控制權交還給調用者,就意味著全部結束。函數中做的所有工作以及保存在局部變量中的數據都將丟失。再次調用這個函數時,一切都將從頭創建。
對于在計算機編程中所討論的函數,這是很標準的流程。這樣的函數只能返回一個值,不過,有時可以創建能產生一個序列的函數還是有幫助的。要做到這一點,這種函數需要能夠“保存自己的工作”。
我說過,能夠“產生一個序列”是因為我們的函數并沒有像通常意義那樣返回。return隱含的意思是函數正將執行代碼的控制權返回給函數被調用的地方。而"yield"的隱含意思是控制權的轉移是臨時和自愿的,我們的函數將來還會收回控制權。
在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
22
23
24
25
26
|
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遍歷到無限大,我們也會在返回第一個值之后卡?。?br />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def get_primes(start): for element in magical_infinite_range(start): if is_prime(element): return element 假設這樣去調用get_primes: 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__()。如同迭代器一樣,我們可以使用next()函數來獲取下一個值。
為了從生成器獲取下一個值,我們使用next()函數,就像對付迭代器一樣。
(next()會操心如何調用生成器的__next__()方法)。既然生成器是一個迭代器,它可以被用在for循環中。
每當生成器被調用的時候,它會返回一個值給調用者。在生成器內部使用yield來完成這個動作(例如yield 7)。為了記住yield到底干了什么,最簡單的方法是把它當作專門給生成器函數用的特殊的return(加上點小魔法)。**
yield就是專門給生成器用的return(加上點小魔法)。
下面是一個簡單的生成器函數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
>>> def simple_generator_function(): >>> yield 1 >>> yield 2 >>> yield 3 這里有兩個簡單的方法來使用它: >>> 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循環,我們創造了自己的無窮串列。
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
15
16
17
|
>>> 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)。
更給力點
在PEP 342中加入了將值傳給生成器的支持。PEP 342加入了新的特性,能讓生成器在單一語句中實現,生成一個值(像從前一樣),接受一個值,或同時生成一個值并接受一個值。
我們用前面那個關于素數的函數來展示如何將一個值傳給生成器。這一次,我們不再簡單地生成比某個數大的素數,而是找出比某個數的等比級數大的最小素數(例如10, 我們要生成比10,100,1000,10000 ... 大的最小素數)。我們從get_primes開始:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
def print_successive_primes(iterations, base = 10 ): # 像普通函數一樣,生成器函數可以接受一個參數 prime_generator = get_primes(base) # 這里以后要加上點什么 for power in range (iterations): # 這里以后要加上點什么 def get_primes(number): while True : if is_prime(number): # 這里怎么寫? get_primes的后幾行需要著重解釋。 yield 關鍵字返回number的值,而像 other = yield foo 這樣的語句的意思是, "返回foo的值,這個值返回給調用者的同時,將other的值也設置為那個值" 。你可以通過send方法來將一個值”發送“給生成器。 def get_primes(number): while True : if is_prime(number): number = yield number number + = 1 |
通過這種方式,我們可以在每次執行yield的時候為number設置不同的值?,F在我們可以補齊print_successive_primes中缺少的那部分代碼:
1
2
3
4
5
|
def print_successive_primes(iterations, base = 10 ): prime_generator = get_primes(base) prime_generator.send( None ) for power in range (iterations): print (prime_generator.send(base * * power)) |
這里有兩點需要注意:首先,我們打印的是generator.send的結果,這是沒問題的,因為send在發送數據給生成器的同時還返回生成器通過yield生成的值(就如同生成器中yield語句做的那樣)。
第二點,看一下prime_generator.send(None)這一行,當你用send來“啟動”一個生成器時(就是從生成器函數的第一行代碼執行到第一個yield語句的位置),你必須發送None。這不難理解,根據剛才的描述,生成器還沒有走到第一個yield語句,如果我們發生一個真實的值,這時是沒有人去“接收”它的。一旦生成器啟動了,我們就可以像上面那樣發送數據了。
綜述
在本系列文章的后半部分,我們將討論一些yield的高級用法及其效果。yield已經成為Python最強大的關鍵字之一?,F在我們已經對yield是如何工作的有了充分的理解,我們已經有了必要的知識,可以去了解yield的一些更“費解”的應用場景。
不管你信不信,我們其實只是揭開了yield強大能力的一角。例如,send確實如前面說的那樣工作,但是在像我們的例子這樣,只是生成簡單的序列的場景下,send幾乎從來不會被用到。下面我貼一段代碼,展示send通常的使用方式。對于這段代碼如何工作以及為何可以這樣工作,在此我并不打算多說,它將作為第二部分很不錯的熱身。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import random def get_data(): """返回0到9之間的3個隨機數""" return random.sample( range ( 10 ), 3 ) def consume(): """顯示每次傳入的整數列表的動態平均值""" running_sum = 0 data_items_seen = 0 while True : data = yield data_items_seen + = len (data) running_sum + = sum (data) print ( 'The running average is {}' . format (running_sum / float (data_items_seen))) def produce(consumer): """產生序列集合,傳遞給消費函數(consumer)""" while True : data = get_data() print ( 'Produced {}' . format (data)) consumer.send(data) yield if __name__ = = '__main__' : consumer = consume() consumer.send( None ) producer = produce(consumer) for _ in range ( 10 ): print ( 'Producing...' ) next (producer) |
請謹記……
我希望您可以從本文的討論中獲得一些關鍵的思想:
- generator是用來產生一系列值的
- yield則像是generator函數的返回結果
- yield唯一所做的另一件事就是保存一個generator函數的狀態
- generator就是一個特殊類型的迭代器(iterator)
- 和迭代器相似,我們可以通過使用next()來從generator中獲取下一個值
- 通過隱式地調用next()來忽略一些值
我希望這篇文章是有益的。如果您還從來沒有聽說過generator,我希望現在您可以理解它是什么以及它為什么是有用的,并且理解如何使用它。如果您已經在某種程度上比較熟悉generator,我希望這篇文章現在可以讓您掃清對generator的一些困惑。