# 練習 30:有限狀態機
> 原文:[Exercise 30: Finite State Machines](https://learncodethehardway.org/more-python-book/ex30.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
> 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻譯](https://translate.google.cn/)
每當你閱讀一本關于解析的書,都有一個可怕的章節,關于有限狀態機(FSM)。他們對“邊”和“節點”進行了詳細的分析,每個可能的“自動機”的組合被轉換成其他自動機,坦率地說,它有點多了。FSM 有一個更簡單的解釋,使得它們實用并且可理解,而不會違背相同主題的純理論版本。當然你不會向 ACM 提交論文,因為你不知道 FSM 背后的所有數學知識,但如果你只想在應用程序中使用它們,那么它們就足夠簡單了。
FSM 是組織事件一種方式,事件發生在一系列狀態上。定義事件的另一種方法是“輸入觸發器”,類似于`if`語句中的布爾表達式,但通常不太復雜。事件可以是按鈕點擊,從流中接收字符,更改日期或時間,以及幾乎任何用于聲明事件的東西。狀態就是你的 FSM 停止的任何“位置”,同時它等待更多的事件,并且你定義的每個狀態都允許事件(輸入)。事件往往是暫時的,而狀態通常是固定的,而且二者都是可以存儲的數據。最后,你可以將代碼附加到事件或狀態,甚至決定在進入狀態時,狀態中或退出狀態時是否應運行代碼。
FSM 只是一種方法,在執行中不同位置發生不同事件時,使用白名單列出可能運行的代碼。在 FSM 中,當你收到意外事件時,你會發生故障,因為你必須明確說明每個狀態允許哪些事件(或條件)。`if`語句也可以處理可能的分支,但它是一個可能性的黑名單。如果你忘記了`else`子句,那么你的`if-elif`條件沒有覆蓋的任何東西都會退回默認。
讓我們將其拆解:
+ 你擁有狀態,這是 FSM 當前所在位置的存儲指示器。狀態可以是“開始”,“按下某鍵”,“中止”或類似的方式,描述執行的可能位置中的 FSM 的位置。每個狀態都意味著正在等待某事發生,在決定下一步做什么之前。
+ 你擁有事件,可以將 FSM 從一個狀態移動到另一個狀態。事件可以是“按下某鍵”,“套接字連接失敗”,“文件保存”,并表示 FSM 接收到一些外部刺激,因此必須決定要做什么,以及下一個狀態是什么。一個事件甚至可以回到同一個狀態,這是你循環的方式。
+ 根據發生的事件,FSM 從一個狀態轉換到另一個狀態,并且僅僅由于為狀態提供的確切事件(盡管其中一個事件可以定義為“任何事件”)。他們不會“意外”轉移狀態,你可以通過查看收到的事件和訪問的狀態,精確地跟蹤他們從一個狀態轉移到另一個狀態。這使得它們非常容易調試。
+ 在狀態轉換之前,之后和期間,你可以在每個事件上運行代碼。這意味著你可以在收到事件時運行一些代碼,然后決定在該狀態下基于該事件做什么,然后在離開該狀態之前再次運行代碼。這種執行代碼的功能使得 FSM 非常強大。
+ 有時候“沒有”也是一個事件。這很好很強大,因為這意味著即使沒有發生任何事情,你也可以將 FSM 轉換到新的狀態。然而,實際上,“沒有”往往是隱含的事件“再來一次”或“醒來”。在其他情況下,這個狀態的意思是,“不確定,也許下一個事件會告訴我是什么狀態。”
FSM 的力量是能夠明確地說明每個事件,事件只是正在接收的數據。這使得它們非常容易進行調試,測試和正確實現,因為你確切地知道每個狀態的可能性,以及在每個狀態中,對于每個事件可能發生的情況。在本練習中,你將要研究 FSM 庫和使用它的 FSM 實現,來了解它們如何工作。
## 挑戰練習
我創建了一個 FSM 模塊,處理一些簡單的事件來處理 Web 服務器的連接。這是一個虛構的 FSM,為你提供一個在 Python 中快速編寫 FSM 的例子。它只是處理連接的基本框架,連接從套接字讀取和寫入,并且它缺少一些重要的東西,但這只是供你使用的一個很小的例子。
```py
def START():
return LISTENING
def LISTENING(event):
if event == "connect":
return CONNECTED
elif event == "error":
return LISTENING
else:
return ERROR
def CONNECTED(event):
if event == "accept":
return ACCEPTED
elif event == "close":
return CLOSED
else:
return ERROR
def ACCEPTED(event):
if event == "close":
return CLOSED
elif event == "read":
return READING(event)
elif event == "write":
return WRITING(event)
else:
return ERROR
def READING(event):
if event == "read":
return READING
elif event == "write":
return WRITING(event)
elif event == "close":
return CLOSED
else:
return ERROR
def WRITING(event):
if event == "read":
return READING(event)
elif event == "write":
return WRITING
elif event == "close":
return CLOSED
else:
return ERROR
def CLOSED(event):
return LISTENING(event)
def ERROR(event):
return ERROR
```
也有一個小測試,向你展示如何運行這個 FSM:
```py
import fsm
def test_basic_connection():
state = fsm.START()
script = ["connect", "accept", "read", "read", "write", "close", "connect"]
for event in script:
print(event, ">>>", state)
state = state(event)
```
你在本練習中的挑戰是,將此示例模塊變成一個更強大和通用的 FSM Python 類。你應該使用它作為一系列線索,來了解如何處理進入的事件,狀態如何作為 Python 函數,以及如何進行隱式的轉換。看看我有時候為下一個狀態返回函數,但其??他時候我會返回一個狀態函數的調用?試著弄清楚為什么我會這樣做,因為它在 FSM 中非常重要。
為了完成這個挑戰,你需要學習 Python [`inspect`](https://docs.python.org/3/library/inspect.html)模塊,看看你可以用 Python 對象和類來做什么。有一些特殊的變量,如`__dict__`以及`inspect`中的函數,可幫助你窺探類或對象并查找函數。
你也可以決定要反轉此設計。你可以將事件作為子類中的函數,并在事件函數內檢查當前的`self.state`,來確定接下來要執行的操作。這完全都取決于你正在處理什么,你是否擁有更多的事件還是狀態,當時什么有意義。
最后,你可以使用一個設計,其中有一個`FSMRunner`類,它只知道如何運行這樣設計的模塊。這比一個知道如何運行自身實例的單一類有一些優點,但也有一些問題。例如,`FSMRunner`如何跟蹤當前狀態?它放在模塊中還是在`FSMRunner`的實例中?
## 研究性學習
+ 使你的測試更加泛用,并為你熟悉的完全不同的領域做一個FSM。
+ 添加一個功能,啟動在你的實現中運行的事件的日志。使用 FSM 處理事件的最大優點之一是,可以存儲和記錄 FSM 收到的所有事件和狀態。這可以讓你調試,為什么它達到你不需要的狀態。
## 深入學習
你應該仔細研究 FSM 背后的數學。我這里的小例子不是完全形式化的概念版本,以便你能理解它。
- 笨辦法學 Python · 續 中文版
- 引言
- 第一部分:預備知識
- 練習 0:起步
- 練習 1:流程
- 練習 2:創造力
- 練習 3:質量
- 第二部分:簡單的黑魔法
- 練習 4:處理命令行參數
- 練習 5:cat
- 練習 6:find
- 練習 7:grep
- 練習 8:cut
- 練習 9:sed
- 練習 10:sort
- 練習 11:uniq
- 練習 12:復習
- 第三部分:數據結構
- 練習 13:單鏈表
- 練習 14:雙鏈表
- 練習 15:棧和隊列
- 練習 16:冒泡、快速和歸并排序
- 練習 17:字典
- 練習 18:性能測量
- 練習 19:改善性能
- 練習 20:二叉搜索樹
- 練習 21:二分搜索
- 練習 22:后綴數組
- 練習 23:三叉搜索樹
- 練習 24:URL 快速路由
- 第四部分:進階項目
- 練習 25:xargs
- 練習 26:hexdump
- 練習 27:tr
- 練習 28:sh
- 練習 29:diff和patch
- 第五部分:文本解析
- 練習 30:有限狀態機
- 練習 31:正則表達式
- 練習 32:掃描器
- 練習 33:解析器
- 練習 34:分析器
- 練習 35:解釋器
- 練習 36:簡單的計算器
- 練習 37:小型 BASIC
- 第六部分:SQL 和對象關系映射
- 練習 38:SQL 簡介
- 練習 39:SQL 創建
- 練習 40:SQL 讀取
- 練習 41:SQL 更新
- 練習 42:SQL 刪除
- 練習 43:SQL 管理
- 練習 44:使用 Python 的數據庫 API
- 練習 45:創建 ORM
- 第七部分:大作業
- 練習 46:blog
- 練習 47:bc
- 練習 48:ed
- 練習 49:sed
- 練習 50:vi
- 練習 51:lessweb
- 練習 52:moreweb