# 練習48.更復雜的用戶輸入
在以前的游戲中,你只是設置一些簡單的預定義字符串作為用戶輸入處理,用戶輸入“run”,程序能正常運行,但是你輸入“run fast”,程序就會運行失敗。我們需要一個設備,它可以識別用戶以各種方式輸入的語匯。例如下面的機種表述都應該被支持才對:
> - open door
> - open the door
> - go THROUGH the door
> - punch bear
> - Punch The Bear in the FACE
也就是說,如果用戶的輸入和常用英語很接近也應該是可以的,而你的游戲要識別出它們的意思。為了達到這個目的,我們將寫一個模塊專門做這件事情。這個模組里邊會有若干個類,它們互相配合,接受用戶輸入,并且將用戶輸入轉換成你的游戲可以識別的命令。
英語的簡單格式是這個樣子的:
> - 單詞由空格隔開。
> - 句子由單詞組成。
> - 語法控制句子的含義。
以最好的開始方式是先搞定如何得到用戶輸入的詞匯,并判斷出它們是什么。
## 我們的游戲詞典
我在游戲里創建了下面這些語匯:
> - 表示方向: north, south, east, west, down, up, left, right, back.
> - 動詞: go, stop, kill, eat.
> - 修飾詞: the, in, of, from, at, it
> - 名詞: door, bear, princess, cabinet.
> - 數字: 由 0-9 構成的數字。
說到名詞,我們會碰到一個小問題,那就是不一樣的房間會用到不一樣的一組名詞,不過讓我們先挑一小組出來寫程序,以后再做改進。
## 如何斷句
我們已經有了詞匯表,為了分析句子的意思,接下來我們需要找到一個斷句的方法。我們對于句子的定義是“空格隔開的單詞”,所以只要這樣就可以了:
~~~
stuff = raw_input('> ')
words = stuff.split()
~~~
目前做到這樣就可以了,不過這招在相當一段時間內都不會有問題。
## 詞匯元組
一旦我們知道了如何將句子轉化成詞匯列表,剩下的就是逐一檢查這些詞匯,看它們是什么類型。為了達到這個目的,我們將用到一個非常好使的 Python 數據結構,叫做”元組(tuple)”。元組其實就是一個不能修改的列表。創建它的方法和創建列表差不多,成員之間需要用逗號隔開,不過方括號要換成圓括號 `()`:
~~~
first_word = ('verb', 'go')
second_word = ('direction', 'north')
third_word = ('direction', 'west')
sentence = [first_word, second_word, third_word]
~~~
這樣我們就創建了一個(TYPE,WORD)組,讓你識別出單詞,并且對它執行指令。
這只是一個例子,不過最后做出來的樣子也差不多。你接受用戶輸入,用 `split`將其分隔成單詞列表,然后分析這些單詞,識別它們的類型,最后重新組成一個句子。
## 掃描輸入
現在你要寫的是詞匯掃描器。這個掃描器會將用戶的輸入字符串當做參數,然后返回由多個 (TOKEN, WORD) 組成的一個列表,這個列表實現類似句子的功能。如果一個單詞不在預定的詞匯表中,那它返回時 WORD 應該還在,但 TOKEN 應該設置成一個專門的錯誤標記。這個錯誤標記將告訴用戶哪里出錯了。
有趣的地方來了。我不會告訴你這些該怎樣做,但我會寫一個“單元測試(unit test)”,而你要把掃描器寫出來,并保證單元測試能夠正常通過。
## 異常和數字
有一件小事情我會先幫幫你,那就是數字轉換。為了做到這一點,我們會作一點弊,使用“異常(exceptions)”來做。“異常”指的是你運行某個函數時得到的錯誤。你的函數在碰到錯誤時,就會“拋出(raise)”一個“異常”,然后你就要去處理(handle)這個異常。假如你在Python 里寫了這些東西:
~~~
Python 2.7.1 (r271:86832, Jun 16 2011, 16:59:05)
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> int("hell")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'hell'
~~~
這個`ValueError`就是`int()`函數拋出的一個異常。因為你給`int()` 的參數不是一個數字。`int()`函數其實也可以返回一個值來告訴你它碰到了錯誤,不過由于它只能返回整數值,所以很難做到這一點。它不能返回 -1,因為這也是一個數字。`int()`沒有糾結在它“究竟應該返回什么”上面,而是提出了一個叫做`ValueError`的異常,然后你只要處理這個異常就可以了。
處理異常的方法是使用`try`和`except`這兩個關鍵字:
~~~
def convert_number(s):
try:
return int(s)
except ValueError:
return None
~~~
你把要試著運行的代碼放到`try`的區段里,再將出錯后要運行的代碼放到 `except`區段里。在這里,我們要試著調用`int()` 去處理某個可能是數字的東西,如果中間出了錯,我們就抓到這個錯誤,然后返回`None`。
在你寫的掃描器里面,你應該使用這個函數來測試某個東西是不是數字。做完這個檢查,你就可以聲明這個單詞是一個錯誤單詞了。
## 測試第一的挑戰
測試首先是一種編程策略,你先寫一段自動化測試代碼,假裝代碼是在正常運行的,然后你再寫出代碼保證測試代碼能正常運行。這種方法用在當你不知道代碼是如何運行,但又可以想象必須使用它的時候。比如說,如果你知道你需要在另一個模塊中使用一個新類,但是你不太知道如何實現這個類,那么先寫出測試程序。
我將給你一份測試代碼,你需要寫出代碼,保證測試代碼能正常工作。為了完成這個任務,你可以看看下面的流程:
> 1. 創建一小部分我給你的測試代碼
> 1. 確保它運行失敗,你知道測試實際上是確認功能的工作原理。
> 1. 到你的源代碼文件`lexicon.py`中,寫出能使測試代碼通過的代碼
> 1. 重復以上工作直到你實現測試中的所有點
當你做到3的時候,和其他編寫代碼的方法相結合也是很好的方法:
> 1. 編寫你需要的函數或類的基本框架
> 1. 添加注釋,解釋說明這個函數是如何運行的
> 1. 按照描述中的注釋寫代碼
> 1. 去掉注釋
這種寫代碼的方法被稱作“psuedo code”,用在你不知道該如何實現某些功能,但是會用自己的語言來描述這個功能的時候。
結合“test first”和“psuedo code”策略,我們得出一個編程的簡易流程:
> 1. 寫一些運行失敗的測試用例
> 1. 寫出測試要用的函數、方法、類的基本結構
> 1. 用自己的語言填充這些框架,解釋它們的功能
> 1. 用代碼替換注釋,直到測試代碼運行通過
> 1. 重復
在這節練習中,你將通過運行我給你的測試程序逆向運行`lexicon.py`來實踐這個方法。
## 你應該測試的東西
這里是你要用到的測試文件:
~~~
from nose.tools import *
from ex48 import lexicon
def test_directions():
assert_equal(lexicon.scan("north"), [('direction', 'north')])
result = lexicon.scan("north south east")
assert_equal(result, [('direction', 'north'),
('direction', 'south'),
('direction', 'east')])
def test_verbs():
assert_equal(lexicon.scan("go"), [('verb', 'go')])
result = lexicon.scan("go kill eat")
assert_equal(result, [('verb', 'go'),
('verb', 'kill'),
('verb', 'eat')])
def test_stops():
assert_equal(lexicon.scan("the"), [('stop', 'the')])
result = lexicon.scan("the in of")
assert_equal(result, [('stop', 'the'),
('stop', 'in'),
('stop', 'of')])
def test_nouns():
assert_equal(lexicon.scan("bear"), [('noun', 'bear')])
result = lexicon.scan("bear princess")
assert_equal(result, [('noun', 'bear'),
('noun', 'princess')])
def test_numbers():
assert_equal(lexicon.scan("1234"), [('number', 1234)])
result = lexicon.scan("3 91234")
assert_equal(result, [('number', 3),
('number', 91234)])
def test_errors():
assert_equal(lexicon.scan("ASDFADFASDF"), [('error', 'ASDFADFASDF')])
result = lexicon.scan("bear IAS princess")
assert_equal(result, [('noun', 'bear'),
('error', 'IAS'),
('noun', 'princess')])
~~~
你需要用項目框架寫出一個新的項目,就像你在練習47中做的一樣。然后你需要創建這個測試用例以及你會用到的`lexicon.py`,看看測試用例頂部,看看它是如何被導入的。
接下來,按照我給你的提示寫一些測試用例。看看我是如何做的:
> 1. 在測試用例頂部寫上導入(import),并保證它正常運行
> 1. 創建第一個測試用例`test_directions`的空版本,并保證它正常運行
> 1. 寫出測試用例`test_directions`的第一行,保證它運行失敗
> 1. 到`lexicon.py`文件,創建一個空的`scan`方法
> 1. 運行測試用例,至少保證`scan`方法運行,即便測試用例運行失敗
> 1. 為`scan`寫出偽代碼注釋,用來說明`scan`如何通過`test_directions`測試
> 1. 寫出與注釋相匹配的代碼,保證`test_directions`測試通過
> 1. 回到方法`test_directions`,寫完剩下的行
> 1. 回到`lexicon.py`中的`scan`方法,補全代碼直到`test_directions`測試通過
> 1. 這樣,當你的第一個測試通過,你移動到下一個測試重復以上步驟。
只要你堅持在每次執行此過程中的一小塊,你可以成功將大問題分解成更小的問題來解決。就像爬山的時候,你把整段路程分成一小段一小段。
## 附加題
> 1. 改進單元測試,讓它覆蓋到更多的語匯。
> 1. 向語匯列表添加更多的語匯,并且更新單元測試代碼。
> 1. 讓你的掃描器能夠識別任意大小寫的詞匯。更新你的單元測試。
> 1. 找出另外一種轉換為數字的方法。
> 1. 我的解決方案用了 37 行代碼,你的是更長還是更短呢?
## 常見問題
### Q: 為什么我一直有這個報錯`ImportErrors`?
> 導入異常通常有以下幾點原因:1,在你的模塊(modules)目錄下沒有生成`__init__.py`文件;2,你在錯誤的目錄下啟動服務;3,你導入的模塊有拼寫錯誤;4,你的`PYTHONPATH`沒有設置成`.`。
### Q: `try-except`和`if-else`有什么區別?
> `try-expect`是用來處理模塊拋出的異常,永遠都不能用`if-else`代替。
### Q: 有沒有辦法能實現在等待用戶輸入的時候,游戲也一樣運行
> 我假設一種情況,你想實現用戶在反應不夠快的情況下會遭到怪物的攻擊,這是可能的,但是它涉及的模塊和技術是本書范圍之外的。
- 序言
- 前言
- 簡介
- 0:安裝和準備
- 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:IF 語句
- 30:Else 和 If
- 31:做出決定
- 32:循環和列表
- 33:while循環
- 34:訪問列表元素
- 35:分支和函數
- 36:設計和調試
- 37:復習符號
- 38:列表操作
- 39:字典,可愛的字典
- 40:模塊, 類和對象
- 41:學會說面向對象
- 42:對象、類、以及從屬關系
- 43:基本的面向對象的分析和設計
- 44:繼承Vs.包含
- 45:你來制作一個游戲
- 46:項目骨架
- 47:自動化測試
- 48:更復雜的用戶輸入
- 49:寫代碼語句
- 50:你的第一個網站
- 51:從瀏覽器獲取輸入
- 52:開始你的web游戲
- 來自老程序員的建議
- 下一步
- 附錄A:命令行教程
- 簡介
- 安裝和準備
- 路徑, 文件夾, 名錄 (pwd)
- 如果你迷路了
- 創建一個路徑 (mkdir)
- 改變當前路徑 (cd)
- 列出當前路徑 (ls)
- 刪除路徑 (rmdir)
- 目錄切換(pushd, popd)
- 生成一個空文件(Touch, New-Item)
- 復制文件 (cp)
- 移動文件 (mv)
- 查看文件 (less, MORE)
- 輸出文件 (cat)
- 刪除文件 (rm)
- 退出命令行 (exit)
- 下一步