# 習題 48: 更復雜的用戶輸入
你的游戲可能一路跑得很爽,不過你處理用戶輸入的方式肯定讓你不勝其煩了。每一個房間都需要一套自己的語句,而且只有用戶完全輸入正確后才能執行。你需要一個設備,它可以允許用戶以各種方式輸入語匯。例如下面的機種表述都應該被支持才對:
- 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 = ('direction', 'north')
second_word = ('verb', 'go')
sentence = [first_word, second_word]
~~~
這樣我們就創建了一個 (TYPE, WORD) 組,讓你識別出單詞,并且對它執行指令。
這只是一個例子,不過最后做出來的樣子也差不多。你接受用戶輸入,用 split 將其分隔成單詞列表,然后分析這些單詞,識別它們的類型,最后重新組成一個句子。
### 掃描輸入
現在你要寫的是詞匯掃描器。這個掃描器會將用戶的輸入字符串當做參數,然后返回由多個 (TOKEN, WORD) 組成的一個列表,這個列表實現類似句子的功能。如果一個單詞不在預定的詞匯表中,那它返回時 WORD 應該還在,但 TOKEN 應該設置成一個專門的錯誤標記。這個錯誤標記將告訴用戶哪里出錯了。
有趣的地方來了。我不會告訴你這些該怎樣做,但我會寫一個“單元測試(unit test)”,而你要把掃描器寫出來,并保證單元測試能夠正常通過。
### “異常”和數字
有一件小事情我會先幫幫你,那就是數字轉換。為了做到這一點,我們會作一點弊,使用“異常(exceptions)”來做。“異常”指的是你運行某個函數時得到的錯誤。你的函數在碰到錯誤時,就會“提出(raise)”一個“異常”,然后你就要去處理(handle)這個異常。假如你在Python 里寫了這些東西:
~~~
~/projects/simplegame $ python
Python 2.6.5 (r265:79063, Apr 16 2010, 13:57:41)
[GCC 4.4.3] on linux2
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。
在你寫的掃描器里面,你應該使用這個函數來測試某個東西是不是數字。做完這個檢查,你就可以聲明這個單詞是一個錯誤單詞了。
### 你應該測試的東西
這里是你應該使用的測試文件 tests/lexicon_tests.py :
<table class="highlighttable"><tbody><tr><td class="linenos"> <div class="linenodiv"> <pre> 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</pre> </div> </td> <td class="code"> <div class="highlight"> <pre>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')])
</pre> </div> </td> </tr></tbody></table>
記住你要使用你的項目骨架來創建新項目,將這個測試用例寫下來(不許復制粘貼!),然后編寫你的掃描器,直至所有的測試都能通過。注意細節并確認結果一切工作良好。
### 設計的技巧
集中一次實現一個測試項目,盡量保持項目簡單,只要把你的 lexicon.py 詞匯表中所有的單詞放那里就可以了。不要修改輸入的單詞表,不過你需要創建自己的新列表,里邊包含你的語匯元組。另外,記得使用 in 關鍵字來檢查這些語匯列表,以確認某個單詞是否在你的語匯表中。
### 加分習題
1. 改進單元測試,讓它覆蓋到更多的語匯。
1. 向語匯列表添加更多的語匯,并且更新單元測試代碼。
1. 讓你的掃描器能夠識別任意大小寫的詞匯。更新你的單元測試以確認其功能。
1. 找出另外一種轉換為數字的方法。
1. 我的解決方案用了 37 行代碼,你的是更長還是更短呢?
- 譯者前言
- 前言:笨辦法更簡單
- 習題 0: 準備工作
- 習題 1: 第一個程序
- 習題 2: 注釋和井號
- 習題 3: 數字和數學計算
- 習題 4: 變量(variable)和命名
- 習題 5: 更多的變量和打印
- 習題 6: 字符串(string)和文本
- 習題 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: 來自 Percal 25 號行星的哥頓人(Gothons)
- 習題 42: 物以類聚
- 習題 43: 你來制作一個游戲
- 習題 44: 給你的游戲打分
- 習題 45: 對象、類、以及從屬關系
- 習題 46: 一個項目骨架
- 習題 47: 自動化測試
- 習題 48: 更復雜的用戶輸入
- 習題 49: 創建句子
- 習題 50: 你的第一個網站
- 習題 51: 從瀏覽器中獲取輸入
- 習題 52: 創建你的 web 游戲
- 下一步
- 老程序員的建議