簡單生成器有許多優(yōu)點。生成器除了能夠用更自然的方法表達一類問題的流程之外,還極大地改善了許多效率不足之處。在 Python 中,函數(shù)調(diào)用代價不菲;除其它因素外,還要花一段時間解決函數(shù)參數(shù)列表(除了其它的事情外,還要分析位置參數(shù)和缺省參數(shù))。初始化框架對象還要采取一些建立步驟(據(jù) Tim Peters 在 comp.lang.python 上所說,有 100 多行 C 語言程序;我自己還沒檢查 Python 源代碼呢)。與此相反,恢復一個生成器就相當省力;參數(shù)已經(jīng)解析完了,而且框架對象正“無所事事地”等待恢復(幾乎不需要額外的初始化)。當然,如果速度是最重要的,您不應該使用字節(jié)碼已編譯過的動態(tài)語言;但即使在速度不是主要考慮因素的情況下,快點總比慢點好。
回憶狀態(tài)機
在“可愛的 Python”前面的另一篇文章中,我介紹了StateMachine 類 ,給定的機器需要多少狀態(tài)處理程序,它就允許用戶添加多少狀態(tài)處理程序。在模型中,將一個或多個狀態(tài)定義為終態(tài)(end state),僅將一個狀態(tài)定義為初始狀態(tài)(start state)(調(diào)用類方法對此進行配置)。每個處理程序都有某種必需的結構;處理程序?qū)?zhí)行一系列操作,然后過一會兒,它帶著一個標記返回到 StateMachine.run() 方法中的循環(huán)內(nèi),該標記指出了想得到的下一個狀態(tài)。同樣,用 cargo 變量允許一個狀態(tài)把一些(未處理的)信息傳遞給下一個狀態(tài)。
我介紹的 StateMachine 類的典型用途是以一個有狀態(tài)的方式使用輸入。例如,我所用的一個文本處理工具(Txt2Html)從一個文件中讀取數(shù)行內(nèi)容;依據(jù)每行所屬的類別,需要以特殊的方式對其進行處理。然而,您經(jīng)常需要看看前面幾行提供的上下文來確定當前行屬于哪個類別(以及應該怎樣處理它)。構建在 StateMachine 類上的這個過程的實現(xiàn)可以定義一個 A 處理程序,該處理程序讀取幾行,然后以類似 A 的方式處理這些行。不久,滿足了一個條件,這樣下一批的幾行內(nèi)容就應該由 B 處理程序來處理了。 A 把控制傳遞回 .run() 循環(huán),同時指示切換到 B 狀態(tài) ― 以及任何 A 不能正確處理的、 B 應該在閱讀額外的幾行之前處理的額外的行。最后,某個處理程序?qū)⑺目刂苽鬟f給某個被指定為終態(tài)的狀態(tài),處理停止(halt)。
對于前面一部分中的具體代碼示例,我使用了一個簡化過的應用程序。我處理由迭代函數(shù)產(chǎn)生的數(shù)字流,而不是處理多行內(nèi)容。每個狀態(tài)處理程序僅打印那些在期望的數(shù)字范圍內(nèi)的數(shù)字(以及關于有效狀態(tài)的一些消息)。當數(shù)字流中的一個數(shù)字傳到一個不同的范圍內(nèi),另一個不同的處理程序就會接管“處理”。對于這一部分,我們將看看另一種用生成器實現(xiàn)相同數(shù)字流處理的方式(有一些額外的技巧和功能)。但是,一個更復雜的生成器示例有可能對更象上一段中提到的輸入流進行處理。我們再來看看前一個狀態(tài)機刪減過代碼的版本:
清單 1. statemachine_test.py
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
|
from statemachine import StateMachine def ones_counter(val): print "ONES State: " , while 1 : if val < = 0 or val > = 30 : newState = "Out_of_Range" ; break elif 20 < = val < 30 : newState = "TWENTIES" ; break elif 10 < = val < 20 : newState = "TENS" ; break else : print " @ %2.1f+" % val, val = math_func(val) print " >>" return (newState, val) # ... other handlers ... def math_func(n): from math import sin return abs (sin(n)) * 31 if __name__ = = "__main__" : m = StateMachine() m.add_state( "ONES" , ones_counter) m.add_state( "TENS" , tens_counter) m.add_state( "TWENTIES" , twenties_counter) m.add_state( "OUT_OF_RANGE" , None , end_state = 1 ) m.set_start( "ONES" ) m.run( 1 ) |
讀者如果接下來對導入的 StateMachine 類以及它的方法如何工作感興趣,應該看看前面的文章。
使用生成器
基于生成器的狀態(tài)機的完整版本比我更愿意在本專欄中介紹的代碼樣本略長。不過,下面的代碼樣本是完整的應用程序,而且不需要導入單獨的 statemachine 模塊以提供支持。總的來說,這個版本比基于類的那個版本要短一些(我們將看到它有一些特別之處,而且還非常強大)。
清單 2. stategen_test.py
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
from __future__ import generators import sys def math_gen(n): # Iterative function becomes a generator from math import sin while 1 : yield n n = abs (sin(n)) * 31 # Jump targets not state-sensitive, only to simplify example def jump_to(val): if 0 < = val < 10 : return 'ONES' elif 10 < = val < 20 : return 'TENS' elif 20 < = val < 30 : return 'TWENTIES' else : return 'OUT_OF_RANGE' def get_ones( iter ): global cargo while 1 : print "\nONES State: " , while jump_to(cargo) = = 'ONES' : print "@ %2.1f " % cargo, cargo = iter . next () yield (jump_to(cargo), cargo) def get_tens( iter ): global cargo while 1 : print "\nTENS State: " , while jump_to(cargo) = = 'TENS' : print "#%2.1f " % cargo, cargo = iter . next () yield (jump_to(cargo), cargo) def get_twenties( iter ): global cargo while 1 : print "\nTWENTIES State: " , while jump_to(cargo) = = 'TWENTIES' : print "*%2.1f " % cargo, cargo = iter . next () yield (jump_to(cargo), cargo) def exit( iter ): jump = raw_input ( '\n\n[co-routine for jump?] ' ).upper() print "...Jumping into middle of" , jump yield (jump, iter . next ()) print "\nExiting from exit()..." sys.exit() def scheduler(gendct, start): global cargo coroutine = start while 1 : (coroutine, cargo) = gendct[coroutine]. next () if __name__ = = "__main__" : num_stream = math_gen( 1 ) cargo = num_stream. next () gendct = { 'ONES' : get_ones(num_stream), 'TENS' : get_tens(num_stream), 'TWENTIES' : get_twenties(num_stream), 'OUT_OF_RANGE' : exit(num_stream) } scheduler(gendct, jump_to(cargo)) |
關于基于生成器的狀態(tài)機,要研究的地方還很多。第一點在很大程度上是表面性的。我們安排 stategen_test.py 只能使用函數(shù),不能使用類(至少按我的意思,生成器更有一種函數(shù)編程的感覺而非面向?qū)ο缶幊蹋∣OP)的感覺)。但是,如果希望的話,您可以很容易地把相同的通用模型包裝到一個或多個類中。
我們的樣本中的主函數(shù)是 scheduler() ,它完全是一般性的(但是比前面的模式中的 StateMachine 要短許多)。函數(shù) scheduler() 要求生成器-迭代器對象字典(“實例化的”生成器)作為參數(shù)。給每個生成器取的字符串名稱可以是您所希望的任意名稱 ― 生成器工廠函數(shù)的字面名稱是一個顯而易見的選擇,但是我在示例中使用大寫的關鍵字名稱。 scheduler() 函數(shù)還接受“初始狀態(tài)”作為參數(shù),但如果您希望的話,也許可以自動選擇一個缺省值。
每個“已調(diào)度的”生成器遵循一些簡單的慣例。每個生成器運行一段時間,然后產(chǎn)生一對值,包含期望的跳轉(zhuǎn)和某個“cargo” ― 就像用前面的模型一樣。沒有生成器被明確地標記為“終態(tài)”。相反,我們允許各個生成器選擇產(chǎn)生錯誤來結束 scheduler() 。特殊情況下,如果生成器“離開”終態(tài)或者到達一個 return 狀態(tài),生成器將產(chǎn)生 StopIteration 異常。如果需要的話,您可以捕獲這個異常(或者是一個不同的異常)。在我們的例子中,我們使用 sys.exit() 來終止應用程序,在 exit() 生成器中會遇到這個 sys.exit()。
要注意關于代碼的兩個小問題。上面的樣本使用一個更簡潔的循環(huán)生成器-迭代器,而不是使用迭代函數(shù)來生成我們的數(shù)字序列。生成器僅隨著每個后續(xù)的調(diào)用發(fā)出一個(無窮的/不確定的)數(shù)字流,而不是連續(xù)返回“最后的值”。這是一個雖然小但卻好用的生成器樣本。而且,上面把“狀態(tài)轉(zhuǎn)換”隔離在了一個單獨的函數(shù)中。在實際程序中,狀態(tài)轉(zhuǎn)變跳轉(zhuǎn)更是上下文相關的,而且可能要在實際的生成器體內(nèi)決定。該途徑簡化了樣本。盡管可能用處不大,但是您姑且聽聽,我們完全可以通過一個函數(shù)工廠產(chǎn)生生成器函數(shù)從而進一步簡化;但是一般情況每個生成器都不會與其它生成器相似到足以使這種方法切實可行。
協(xié)同程序和半?yún)f(xié)同程序
細心的讀者可能注意到了,實際上我們不知不覺地進入了一種比最初所表明的要有用得多的流控制結構。在樣本代碼中,不僅僅只是有了狀態(tài)機。事實上,上面的模式是一個很有效的協(xié)同程序通用的系統(tǒng)。大多數(shù)讀者在此或許會需要一些背景知識。
協(xié)同程序是程序功能的集合,它允許任意地分支到其它的控制上下文中 以及從分支點任意恢復流。我們在大多數(shù)編程語言中所熟悉的子例程是通用協(xié)同程序的一種極為有限的分支情況。子例程僅從頂端的一個固定點進入并且只退出一次(它不能被恢復)。子例程還總是把流傳送回它的調(diào)用者處。本質(zhì)上,每個協(xié)同程序代表一個可調(diào)用的延續(xù) ― 盡管添加一個新的單詞并不一定能向不知道這個單詞的人闡明它的意思。Randall HydeAn 的 The Art of Assembly中的“Cocall Sequence Between Two Processes”插圖對于解釋協(xié)同程序大有幫助。 參考資料上有到此圖的鏈接。參考資料中還有到 Hyde 的綜合討論的鏈接,該討論相當不錯。
不管算不算負面影響,您還是會注意到,在許多語言中臭名昭著的 goto 語句也允許任意分支,但是在一個不太結構化的上下文中,它能導致“通心粉 代碼”。
Python 2.2+ 的生成器向協(xié)同程序邁進了一大步。這一大步是指,生成器 ― 和函數(shù)/子例程不同 ― 是可恢復的,并且可以在多個調(diào)用之后得到值。然而,Python 生成器只不過是 Donald Knuth 所描述的“半?yún)f(xié)同程序”。生成器是可恢復的,并且可以在別處分支控制 ― 但是它只能分支控制回到直接調(diào)用它的調(diào)用者處。確切的說,生成器上下文(和任何上下文一樣)可以自己調(diào)用其它生成器或函數(shù) ― 甚至可以它自己進行遞歸調(diào)用 ― 但是每個最終的返回必須經(jīng)由返回上下文的線性層次結構傳遞。Python 生成器不考慮“生產(chǎn)者”和“消費者”的常見協(xié)同程序用法(可以隨意從對方的中間位置繼續(xù))。
幸運的是,用 Python 生成器模仿配備齊全的的協(xié)同程序相當容易。簡單的竅門就是和上面樣本代碼中生成器十分類似的 scheduler() 函數(shù)。事實上,我們所提出的狀態(tài)機本身就是一個常見得多的協(xié)同程序框架模式。適應這種模式能克服 Python 生成器中仍存在的小缺陷(讓粗心大意的程序員也能發(fā)揮出通心粉代碼的全部力量)。
操作中的 Stategen
要想準確了解 stategen_test.py 中發(fā)生了什么,最簡單的辦法就是運行它:
清單 3. 運行 STATEGEN(手工跳轉(zhuǎn)控制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
% python stategen_test.py ONES State: @ 1.0 TWENTIES State: * 26.1 * 25.3 ONES State: @ 4.2 TWENTIES State: * 26.4 * 29.5 * 28.8 TENS State: #15.2 #16.3 #16.0 ONES State: @ 9.5 @ 3.8 TENS State: #18.2 #17.9 TWENTIES State: * 24.4 TENS State: #19.6 TWENTIES State: * 21.4 TENS State: #16.1 #12.3 ONES State: @ 9.2 @ 6.5 @ 5.7 TENS State: #16.7 TWENTIES State: * 26.4 * 30.0 [co - routine for jump?] twenties ...Jumping into middle of TWENTIES TWENTIES State: TENS State: #19.9 TWENTIES State: * 26.4 * 29.4 * 27.5 * 22.7 TENS State: #19.9 TWENTIES State: * 26.2 * 26.8 Exiting from exit()... |
這個輸出和前面的 statemachine_test.py 中的輸出基本上是完全相同的。結果中的每一行分別表示在特定的處理程序或生成器中使用的流;在行的開頭聲明了流上下文。但是,每當另一個協(xié)同程序分支轉(zhuǎn)到生成器內(nèi)時,生成器版本 恢復執(zhí)行(在一個循環(huán)內(nèi)),而不僅僅是再次 調(diào)用處理程序函數(shù)。假設所有的 get_*() 協(xié)同程序體都包含在無限循環(huán)中,這點差異就不那么明顯了。
要了解 stategen_test.py 中的本質(zhì)差異,看看 exit() 生成器中發(fā)生了什么。第一次調(diào)用生成器-迭代器時,從用戶處收集一個跳轉(zhuǎn)目標(這是現(xiàn)實中的應用中有可能利用的事件驅(qū)動分支決策的一種簡單情況)。然而,當再次調(diào)用 exit() 時,它位于生成器的一個稍后的流上下文中 ― 顯示退出消息,并調(diào)用 sys.exit() 。交互作用樣本中的用戶完全可以直接跳轉(zhuǎn)到“out_of_range”,不用轉(zhuǎn)到另一個“處理程序”就退出(但是它 將執(zhí)行一個到這個相同生成器內(nèi)的遞歸跳轉(zhuǎn))。
結束語
我在介紹中說過,我期望狀態(tài)機版本的協(xié)同程序運行速度大大超過前面介紹的帶回調(diào)處理程序的類(class-with-callback-handler)"版本的速度。恢復生成器-迭代器效率要高得多。特定的示例如此簡單,幾乎不足以作為評判標準,但是我歡迎讀者對具體結果進行反饋。
但不管我介紹的“協(xié)同程序模式”在速度方面可能取得什么樣的進展,在它實現(xiàn)的驚人的通用流控制面前都會黯然失色。comp.lang.python 新聞組上的許多讀者都曾詢問過 Python 的新生成器有多通用。我想,我所描述的框架的可用性作了回答:“和您想要的一樣!”對于大多數(shù)和 Python 有關的事情,對某些事情 編程通常比 理解它們要簡單得多。試試我的模式;我想您會發(fā)現(xiàn)它很有用。