簡單理解Python中基於產生器的狀態機器_python

來源:互聯網
上載者:User

 簡單產生器有許多優點。產生器除了能夠用更自然的方法表達一類問題的流程之外,還極大地改善了許多效率不足之處。在 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 狀態 ― 以及任何 A 不能正確處理的、 B 應該在閱讀額外的幾行之前處理的額外的行。最後,某個處理常式將它的控制傳遞給某個被指定為終態的狀態,處理停止(halt)。

對於前面一部分中的具體程式碼範例,我使用了一個簡化過的應用程式。我處理由迭代函數產生的數字流,而不是處理多行內容。每個狀態處理常式僅列印那些在期望的數字範圍內的數字(以及關於有效狀態的一些訊息)。當數字流中的一個數字傳到一個不同的範圍內,另一個不同的處理常式就會接管“處理”。對於這一部分,我們將看看另一種用產生器實現相同數字流處理的方式(有一些額外的技巧和功能)。但是,一個更複雜的產生器樣本有可能對更象上一段中提到的輸入資料流進行處理。我們再來看看前一個狀態機器刪減過代碼的版本:
清單 1. statemachine_test.py

from statemachine import StateMachinedef 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))*31if __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 generatorsimport sysdef 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 exampledef 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() 要求產生器-迭代器對象字典(“執行個體化的”產生器)作為參數。給每個產生器取的字串名稱可以是您所希望的任意名稱 ― 產生器工廠函數的字面名稱是一個顯而易見的選擇,但是我在樣本中使用大寫的關鍵字名稱。 scheduler() 函數還接受“初始狀態”作為參數,但如果您希望的話,也許可以自動選擇一個預設值。

每個“已調度的”產生器遵循一些簡單的慣例。每個產生器運行一段時間,然後產生一對值,包含期望的跳轉和某個“cargo” ― 就像用前面的模型一樣。沒有產生器被明確地標記為“終態”。相反,我們允許各個產生器選擇產生錯誤來結束 scheduler() 。特殊情況下,如果產生器“離開”終態或者到達一個 return 狀態,產生器將產生 StopIteration 異常。如果需要的話,您可以捕獲這個異常(或者是一個不同的異常)。在我們的例子中,我們使用 sys.exit() 來終止應用程式,在 exit() 產生器中會遇到這個 sys.exit()。

要注意關於代碼的兩個小問題。上面的樣本使用一個更簡潔的迴圈產生器-迭代器,而不是使用迭代函數來產生我們的數字序列。產生器僅隨著每個後續的調用發出一個(無窮的/不確定的)數字流,而不是連續返回“最後的值”。這是一個雖然小但卻好用的產生器樣本。而且,上面把“狀態轉換”隔離在了一個單獨的函數中。在實際程式中,狀態轉變跳轉更是上下文相關的,而且可能要在實際的產生器體內決定。該途徑簡化了樣本。儘管可能用處不大,但是您姑且聽聽,我們完全可以通過一個函數工廠產生產生器函數從而進一步簡化;但是一般情況每個產生器都不會與其它產生器相似到足以使這種方法切實可行。

協同程式和半協同程式

細心的讀者可能注意到了,實際上我們不知不覺地進入了一種比最初所表明的要有用得多的流量控制結構。在樣本代碼中,不僅僅只是有了狀態機器。事實上,上面的模式是一個很有效協同程式通用的系統。大多數讀者在此或許會需要一些背景知識。

協同程式是程式功能的集合,它允許任意地分支到其它的控制上下文中 以及從分支點任意恢複流。我們在大多數程式設計語言中所熟悉的子常式是通用協同程式的一種極為有限的分支情況。子常式僅從頂端的一個固定點進入並且只退出一次(它不能被恢複)。子常式還總是把流傳送回它的調用者處。本質上,每個協同程式代表一個可調用的延續 ― 儘管添加一個新的單詞並不一定能向不知道這個單詞的人闡明它的意思。Randall HydeAn 的 The Art of Assembly中的“Cocall Sequence Between Two Processes”插圖對於解釋協同程式大有協助。 參考資料上有到此圖的連結。參考資料中還有到 Hyde 的綜合討論的連結,該討論相當不錯。

不管算不算負面影響,您還是會注意到,在許多語言中臭名昭著的 goto 語句也允許任意分支,但是在一個不太結構化的上下文中,它能導致“通心粉 代碼”。

Python 2.2+ 的產生器向協同程式邁進了一大步。這一大步是指,產生器 ― 和函數/子常式不同 ― 是可恢複的,並且可以在多個調用之後得到值。然而,Python 產生器只不過是 Donald Knuth 所描述的“半協同程式”。產生器是可恢複的,並且可以在別處分支控制 ― 但是它只能分支控制回到直接調用它的調用者處。確切的說,產生器上下文(和任何上下文一樣)可以自己調用其它產生器或函數 ― 甚至可以它自己進行遞迴調用 ― 但是每個最終的返回必須經由返回內容相關的線性階層傳遞。Python 產生器不考慮“生產者”和“消費者”的常見協同程式用法(可以隨意從對方的中間位置繼續)。

幸運的是,用 Python 產生器模仿配備齊全的的協同程式相當容易。簡單的竅門就是和上面樣本代碼中產生器十分類似的 scheduler() 函數。事實上,我們所提出的狀態機器本身就是一個常見得多的協同程式架構模式。適應這種模式能克服 Python 產生器中仍存在的小缺陷(讓粗心大意的程式員也能發揮出通心粉代碼的全部力量)。


操作中的 Stategen

要想準確瞭解 stategen_test.py 中發生了什麼,最簡單的辦法就是運行它:
清單 3. 運行 STATEGEN(手工跳轉控制)

% python stategen_test.pyONES State:    @ 1.0TWENTIES State:  *26.1  *25.3ONES State:    @ 4.2TWENTIES State:  *26.4  *29.5  *28.8TENS State:    #15.2  #16.3  #16.0ONES State:    @ 9.5  @ 3.8TENS State:    #18.2  #17.9TWENTIES State:  *24.4TENS State:    #19.6TWENTIES State:  *21.4TENS State:    #16.1  #12.3ONES State:    @ 9.2  @ 6.5  @ 5.7TENS State:    #16.7TWENTIES State:  *26.4  *30.0[co-routine for jump?] twenties ...Jumping into middle of TWENTIESTWENTIES State:TENS State:    #19.9TWENTIES State:  *26.4  *29.4  *27.5  *22.7TENS State:    #19.9TWENTIES State:  *26.2  *26.8Exiting from exit()...

這個輸出和前面的 statemachine_test.py 中的輸出基本上是完全相同的。結果中的每一行分別表示在特定的處理常式或產生器中使用的流;在行的開頭聲明了流上下文。但是,每當另一個協同程式分支轉到產生器內時,產生器版本 恢複執行(在一個迴圈內),而不僅僅是再次 調用處理常式函數。假設所有的 get_*() 協同程式體都包含在無限迴圈中,這點差異就不那麼明顯了。

要瞭解 stategen_test.py 中的本質差異,看看 exit() 產生器中發生了什麼。第一次調用產生器-迭代器時,從使用者處收集一個跳轉目標(這是現實中的應用中有可能利用的事件驅動分支決策的一種簡單情況)。然而,當再次調用 exit() 時,它位於產生器的一個稍後的流上下文中 ― 顯示退出訊息,並調用 sys.exit() 。互動作用樣本中的使用者完全可以直接跳轉到“out_of_range”,不用轉到另一個“處理常式”就退出(但是它 將執行一個到這個相同產生器內的遞迴跳轉)。


結束語

我在介紹中說過,我期望狀態機器版本的協同程式運行速度大大超過前面介紹的帶回調處理常式的類(class-with-callback-handler)"版本的速度。恢複產生器-迭代器效率要高得多。特定的樣本如此簡單,幾乎不足以作為評判標準,但是我歡迎讀者對具體結果進行反饋。

但不管我介紹的“協同程式模式”在速度方面可能取得什麼樣的進展,在它實現的驚人的通用流量控制面前都會黯然失色。comp.lang.python 新聞群組上的許多讀者都曾詢問過 Python 的新產生器有多通用。我想,我所描述的架構的可用性作了回答:“和您想要的一樣!”對於大多數和 Python 有關的事情,對某些事情 編程通常比 理解它們要簡單得多。試試我的模式;我想您會發現它很有用。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.