?簡單生成器有許多優點。生成器除了能夠用更自然的方法表達一類問題的流程之外,還極大地改善了許多效率不足之處。在 Python 中,函數調用代價不菲;除其它因素外,還要花一段時間解決函數參數列表(除了其它的事情外,還要分析位置參數和缺省參數)。初始化框架對象還要采取一些建立步驟(據 Tim Peters 在 comp.lang.python 上所說,有 100 多行 C 語言程序;我自己還沒檢查 Python 源代碼呢)。與此相反,恢復一個生成器就相當省力;參數已經解析完了,而且框架對象正“無所事事地”等待恢復(幾乎不需要額外的初始化)。當然,如果速度是最重要的,您不應該使用字節碼已編譯過的動態語言;但即使在速度不是主要考慮因素的情況下,快點總比慢點好。
回憶狀態機
在“可愛的 Python”前面的另一篇文章中,我介紹了StateMachine 類 ,給定的機器需要多少狀態處理程序,它就允許用戶添加多少狀態處理程序。在模型中,將一個或多個狀態定義為終態(end state),僅將一個狀態定義為初始狀態(start state)(調用類方法對此進行配置)。每個處理程序都有某種必需的結構;處理程序將執行一系列操作,然后過一會兒,它帶著一個標記返回到 StateMachine.run() 方法中的循環內,該標記指出了想得到的下一個狀態。同樣,用 cargo 變量允許一個狀態把一些(未處理的)信息傳遞給下一個狀態。
我介紹的 StateMachine 類的典型用途是以一個有狀態的方式使用輸入。例如,我所用的一個文本處理工具(Txt2Html)從一個文件中讀取數行內容;依據每行所屬的類別,需要以特殊的方式對其進行處理。然而,您經常需要看看前面幾行提供的上下文來確定當前行屬于哪個類別(以及應該怎樣處理它)。構建在 StateMachine 類上的這個過程的實現可以定義一個 A 處理程序,該處理程序讀取幾行,然后以類似 A 的方式處理這些行。不久,滿足了一個條件,這樣下一批的幾行內容就應該由 B 處理程序來處理了。 A 把控制傳遞回 .run() 循環,同時指示切換到 B 狀態 ?D 以及任何 A 不能正確處理的、 B 應該在閱讀額外的幾行之前處理的額外的行。最后,某個處理程序將它的控制傳遞給某個被指定為終態的狀態,處理停止(halt)。
對于前面一部分中的具體代碼示例,我使用了一個簡化過的應用程序。我處理由迭代函數產生的數字流,而不是處理多行內容。每個狀態處理程序僅打印那些在期望的數字范圍內的數字(以及關于有效狀態的一些消息)。當數字流中的一個數字傳到一個不同的范圍內,另一個不同的處理程序就會接管“處理”。對于這一部分,我們將看看另一種用生成器實現相同數字流處理的方式(有一些額外的技巧和功能)。但是,一個更復雜的生成器示例有可能對更象上一段中提到的輸入流進行處理。我們再來看看前一個狀態機刪減過代碼的版本:
清單 1. statemachine_test.py
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 類以及它的方法如何工作感興趣,應該看看前面的文章。
使用生成器
基于生成器的狀態機的完整版本比我更愿意在本專欄中介紹的代碼樣本略長。不過,下面的代碼樣本是完整的應用程序,而且不需要導入單獨的 statemachine 模塊以提供支持。總的來說,這個版本比基于類的那個版本要短一些(我們將看到它有一些特別之處,而且還非常強大)。
清單 2. stategen_test.py
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))
關于基于生成器的狀態機,要研究的地方還很多。第一點在很大程度上是表面性的。我們安排 stategen_test.py 只能使用函數,不能使用類(至少按我的意思,生成器更有一種函數編程的感覺而非面向對象編程(OOP)的感覺)。但是,如果希望的話,您可以很容易地把相同的通用模型包裝到一個或多個類中。
我們的樣本中的主函數是 scheduler() ,它完全是一般性的(但是比前面的模式中的 StateMachine 要短許多)。函數 scheduler() 要求生成器-迭代器對象字典(“實例化的”生成器)作為參數。給每個生成器取的字符串名稱可以是您所希望的任意名稱 ?D 生成器工廠函數的字面名稱是一個顯而易見的選擇,但是我在示例中使用大寫的關鍵字名稱。 scheduler() 函數還接受“初始狀態”作為參數,但如果您希望的話,也許可以自動選擇一個缺省值。
每個“已調度的”生成器遵循一些簡單的慣例。每個生成器運行一段時間,然后產生一對值,包含期望的跳轉和某個“cargo” ?D 就像用前面的模型一樣。沒有生成器被明確地標記為“終態”。相反,我們允許各個生成器選擇產生錯誤來結束 scheduler() 。特殊情況下,如果生成器“離開”終態或者到達一個 return 狀態,生成器將產生 StopIteration 異常。如果需要的話,您可以捕獲這個異常(或者是一個不同的異常)。在我們的例子中,我們使用 sys.exit() 來終止應用程序,在 exit() 生成器中會遇到這個 sys.exit()。
要注意關于代碼的兩個小問題。上面的樣本使用一個更簡潔的循環生成器-迭代器,而不是使用迭代函數來生成我們的數字序列。生成器僅隨著每個后續的調用發出一個(無窮的/不確定的)數字流,而不是連續返回“最后的值”。這是一個雖然小但卻好用的生成器樣本。而且,上面把“狀態轉換”隔離在了一個單獨的函數中。在實際程序中,狀態轉變跳轉更是上下文相關的,而且可能要在實際的生成器體內決定。該途徑簡化了樣本。盡管可能用處不大,但是您姑且聽聽,我們完全可以通過一個函數工廠產生生成器函數從而進一步簡化;但是一般情況每個生成器都不會與其它生成器相似到足以使這種方法切實可行。
協同程序和半協同程序
細心的讀者可能注意到了,實際上我們不知不覺地進入了一種比最初所表明的要有用得多的流控制結構。在樣本代碼中,不僅僅只是有了狀態機。事實上,上面的模式是一個很有效的協同程序通用的系統。大多數讀者在此或許會需要一些背景知識。
協同程序是程序功能的集合,它允許任意地分支到其它的控制上下文中 以及從分支點任意恢復流。我們在大多數編程語言中所熟悉的子例程是通用協同程序的一種極為有限的分支情況。子例程僅從頂端的一個固定點進入并且只退出一次(它不能被恢復)。子例程還總是把流傳送回它的調用者處。本質上,每個協同程序代表一個可調用的延續 ?D 盡管添加一個新的單詞并不一定能向不知道這個單詞的人闡明它的意思。Randall HydeAn 的 The Art of Assembly中的“Cocall Sequence Between Two Processes”插圖對于解釋協同程序大有幫助。 參考資料上有到此圖的鏈接。參考資料中還有到 Hyde 的綜合討論的鏈接,該討論相當不錯。
不管算不算負面影響,您還是會注意到,在許多語言中臭名昭著的 goto 語句也允許任意分支,但是在一個不太結構化的上下文中,它能導致“通心粉 代碼”。
Python 2.2+ 的生成器向協同程序邁進了一大步。這一大步是指,生成器 ?D 和函數/子例程不同 ?D 是可恢復的,并且可以在多個調用之后得到值。然而,Python 生成器只不過是 Donald Knuth 所描述的“半協同程序”。生成器是可恢復的,并且可以在別處分支控制 ?D 但是它只能分支控制回到直接調用它的調用者處。確切的說,生成器上下文(和任何上下文一樣)可以自己調用其它生成器或函數 ?D 甚至可以它自己進行遞歸調用 ?D 但是每個最終的返回必須經由返回上下文的線性層次結構傳遞。Python 生成器不考慮“生產者”和“消費者”的常見協同程序用法(可以隨意從對方的中間位置繼續)。
幸運的是,用 Python 生成器模仿配備齊全的的協同程序相當容易。簡單的竅門就是和上面樣本代碼中生成器十分類似的 scheduler() 函數。事實上,我們所提出的狀態機本身就是一個常見得多的協同程序框架模式。適應這種模式能克服 Python 生成器中仍存在的小缺陷(讓粗心大意的程序員也能發揮出通心粉代碼的全部力量)。
操作中的 Stategen
要想準確了解 stategen_test.py 中發生了什么,最簡單的辦法就是運行它:
清單 3. 運行 STATEGEN(手工跳轉控制)
% 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 中的輸出基本上是完全相同的。結果中的每一行分別表示在特定的處理程序或生成器中使用的流;在行的開頭聲明了流上下文。但是,每當另一個協同程序分支轉到生成器內時,生成器版本 恢復執行(在一個循環內),而不僅僅是再次 調用處理程序函數。假設所有的 get_*() 協同程序體都包含在無限循環中,這點差異就不那么明顯了。
要了解 stategen_test.py 中的本質差異,看看 exit() 生成器中發生了什么。第一次調用生成器-迭代器時,從用戶處收集一個跳轉目標(這是現實中的應用中有可能利用的事件驅動分支決策的一種簡單情況)。然而,當再次調用 exit() 時,它位于生成器的一個稍后的流上下文中 ?D 顯示退出消息,并調用 sys.exit() 。交互作用樣本中的用戶完全可以直接跳轉到“out_of_range”,不用轉到另一個“處理程序”就退出(但是它 將執行一個到這個相同生成器內的遞歸跳轉)。
結束語
我在介紹中說過,我期望狀態機版本的協同程序運行速度大大超過前面介紹的帶回調處理程序的類(class-with-callback-handler)"版本的速度。恢復生成器-迭代器效率要高得多。特定的示例如此簡單,幾乎不足以作為評判標準,但是我歡迎讀者對具體結果進行反饋。
但不管我介紹的“協同程序模式”在速度方面可能取得什么樣的進展,在它實現的驚人的通用流控制面前都會黯然失色。comp.lang.python 新聞組上的許多讀者都曾詢問過 Python 的新生成器有多通用。我想,我所描述的框架的可用性作了回答:“和您想要的一樣!”對于大多數和 Python 有關的事情,對某些事情 編程通常比 理解它們要簡單得多。試試我的模式;我想您會發現它很有用。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
