# 練習49.寫代碼語句
從這個小游戲的詞匯掃描器中,我們應該可以得到類似下面的列表:
~~~
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.
>>> from ex48 import lexicon
>>> lexicon.scan("go north")
[('verb', 'go'), ('direction', 'north')]
>>> lexicon.scan("kill the princess")
[('verb', 'kill'), ('stop', 'the'), ('noun', 'princess')]
>>> lexicon.scan("eat the bear")
[('verb', 'eat'), ('stop', 'the'), ('noun', 'bear')]
>>> lexicon.scan("open the door and smack the bear in the nose")
[('error', 'open'), ('stop', 'the'), ('error', 'door'), ('error', 'and'), ('error', 'smack'), ('stop', 'the'), ('noun', 'bear'), ('stop', 'in'), ('stop', 'the'), ('error', 'nose')]
~~~
現在讓我們把它轉化成游戲可以使用的東西,也就是一個`Sentence`類。
如果你還記得學校學過的東西的話,一個句子是由這樣的結構組成的:主語(Subject) + 謂語(動詞 Verb) + 賓語(Object)
很顯然實際的句子可能會比這復雜,而你可能已經在英語的語法課上面被折騰得夠嗆了。我們的目的,是將上面的元組列表轉換為一個`Sentence` 對象,而這個對象又包含主謂賓各個成員。
## 匹配(Match)和窺視(Peek)
為了達到這個效果,你需要四(五)樣工具:
> 1. 循環訪問元組列表的方法,這挺簡單的。
> 1. 匹配我們的主謂賓設置中不同種類元組的方法。
> 1. 一個“窺視”潛在元組的方法,以便做決定時用到。
> 1. 跳過(skip)我們不在乎的內容的方法,例如形容詞、冠詞等沒有用處的詞匯。
> 1. 一個用來存放最終結果的`Sentence`對象
我們要把這些函數放在一個叫做`ex48.parser`的類中,再把這個類放在`ex48/parser.py`中,以便于我們能夠測試它們。我們使用`peek`函數來查看元組列表中的下一個成員,做匹配以后再對它做下一步動作。
## 句子的語法
在你寫代碼之前,你要弄明白一個基礎的英語句子的語法是如何工作的。在我們的練習中,我們準備創建一個叫做`Sentence`的類,它有如下3個屬性:
Sentence.subject(句子的主語)這是任意一個句子的主語,大部分時候可以默認為“玩家player”,比如一個句子“run north 向北跑”, 也就是說 "player run north 玩家向北跑"。主語應該是一個名詞。
Sentence.verb(句子的謂語)這就是句子的的作用。 在 "run north" 中,謂語應該是 "run". 謂語應該是一個動詞。
Sentence.object(句子的賓語) 這又是一個名詞,指的是動詞做了什么。在我們游戲中, 我們分辨出的方向就是賓語。在 "run north" 中,單詞"north"就是賓語。在 "hit bear" 中,單詞"bear" 就是賓語。
我們的程序解析器使用我們給出的函數并返回解析后的句子,轉換成一個`list`或`Sentence`對象,用來接收匹配用戶輸入
## 關于異常(Exception)
已經簡單學過關于異常的一些東西,但還沒學過怎樣拋出(raise)它們。這節的代碼演示了如何 raise 前面定義的`ParserError`。注意`ParserError` 是一個定義為`Exception` 類型的 class。另外要注意我們是怎樣使用`raise` 這個關鍵字來拋出異常的。
你的測試代碼應該也要測試到這些異常,這個我也會演示給你如何實現。
## 程序代碼
如果你希望更大的挑戰,停在這里,然后只聽我的描述來完成代碼。當你遇到難題的時候,可以再回來看看是我如何做的。不過,嘗試自己實現代碼功能對你來說真的是個很好的鍛煉。我要開始串講我的代碼了,你可以開始在自己的`ex48/parser.py`中輸入代碼。我們從異常處理開始我們的代碼編寫:
~~~
class ParserError(Exception):
pass
~~~
這就是你創建一個可以拋出的異常類`ParserError`,接下來,我們需要一個句子類 `Sentence`:
~~~
class Sentence(object):
def __init__(self, subject, verb, obj):
# remember we take ('noun','princess') tuples and convert them
self.subject = subject[1]
self.verb = verb[1]
self.object = obj[1]
~~~
到目前為止,我們沒有寫什么特別的代碼,只是創建了兩個簡單的類。
在我們的問題描述中,我們需要一個函數用來看到列表中的單詞并返回單詞的類型:
~~~
def peek(word_list):
if word_list:
word = word_list[0]
return word[0]
else:
return None
~~~
我們需要這個函數是因為,我們要基于下一個單詞來選擇確認我們要處理的句子是什么,然后我們可以調用另一個函數來處理這個單詞,并將程序繼續下去。
我們使用`match`函數來處理單詞,用它來確認預期中的單詞是否是正確的類型,將它移出列表,并返回該詞:
~~~
def match(word_list, expecting):
if word_list:
word = word_list.pop(0)
if word[0] == expecting:
return word
else:
return None
else:
return None
~~~
相當簡單是不是,不過還是要確認你理解了這些代碼以及為什么我是這么寫的。我需要依據我看到的列表中的下一個單詞來決定我現在處理的句子的類型,然后再用這個單詞創建我的`Sentence`.
最后,我們需要一個方法來跳過句子中我們不關心的單詞。這些單詞會被打上“停用詞”(stop類型的詞)的標簽,比如"the","and"以及"a"等:
~~~
def skip(word_list, word_type):
while peek(word_list) == word_type:
match(word_list, word_type)
~~~
記住`skip`不只跳過一個單詞而是跳過所有該類型的詞,也就是說,如果有人輸入了“scream at the bear”,經過處理最后會得到"scream" 和 "bear".
以上是我們分析函數的基本結構,我可以用它們來處理我們需要的任何文本,盡管我們的程序非常簡單,剩下的函數也都是非常短的。
首先,我們來完成解析動詞的部分:
~~~
def parse_verb(word_list):
skip(word_list, 'stop')
if peek(word_list) == 'verb':
return match(word_list, 'verb')
else:
raise ParserError("Expected a verb next.")
~~~
我們跳過所有"stop"類型的詞,然后提前獲得下一個單詞,并確認它是"verb"類型,如果不是,則拋出一個異常`ParserError`說明為什么不是。如果是"verb"類型,則使用"match"處理,將它移出列表。一個處理"sentence"類的類似函數:
~~~
def parse_object(word_list):
skip(word_list, 'stop')
next_word = peek(word_list)
if next_word == 'noun':
return match(word_list, 'noun')
elif next_word == 'direction':
return match(word_list, 'direction')
else:
raise ParserError("Expected a noun or direction next.")
~~~
重復操作,跳過"stop"類型的詞,提前判斷下一個詞,決定下一個"sentence".在函數`parse_object`中,我們需要同時處理“名詞”和類似賓語的“方向”,解析主語的方法也是一樣的,但是當我們處理隱藏的名詞"player"的時候,我們需要用到"peek":
~~~
def parse_subject(word_list):
skip(word_list, 'stop')
next_word = peek(word_list)
if next_word == 'noun':
return match(word_list, 'noun')
elif next_word == 'verb':
return ('noun', 'player')
else:
raise ParserError("Expected a verb next.")
~~~
所有的方式都準備好之后,我們最后一個函數`parse_sentence`也是非常簡單的:
~~~
def parse_sentence(word_list):
subj = parse_subject(word_list)
verb = parse_verb(word_list)
obj = parse_object(word_list)
return Sentence(subj, verb, obj)
~~~
## 試玩這個游戲
為了弄明白程序是如何運行,你可以像這樣試玩:
~~~
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.
>>> from ex48.parser import *
>>> x = parse_sentence([('verb', 'run'), ('direction', 'north')])
>>> x.subject
'player'
>>> x.verb
'run'
>>> x.object
'north'
>>> x = parse_sentence([('noun', 'bear'), ('verb', 'eat'), ('stop', 'the'), ('noun', 'honey')])
>>> x.subject
'bear'
>>> x.verb
'eat'
>>> x.object
'honey'
~~~
## 你應該測試的東西
為《習題 49》寫一個完整的測試方案,確認代碼中所有的東西都能正常工作,把測試代碼放到文件`tests/parser_tests.py`中,測試代碼中也要包含對異常的測試——輸入一個錯誤的句子它會拋出一個異常來。
使用`assert_raises`這個函數來檢查異常,在 `nose` 的文檔里查看相關的內容,學著使用它寫針對“執行失敗”的測試,這也是測試很重要的一個方面。從`nose`文檔中學會`assert_raises`以及一些別的函數的使用方法。
寫完測試以后,你應該就明白了這段程序的工作原理,而且也學會了如何為別人的程序寫測試代碼。 相信我,這是一個非常有用的技能。
## 附加題
> 1. 修改 `parse_`函數(方法),將它們放到一個類里邊,而不僅僅是獨立的方法函數。這兩種程序設計你喜歡哪一種呢?
> 1. 提高 `parser`的容錯能力,這樣即使用戶輸入了你預定義語匯之外的詞語,你的程序也能正常運行下去。
> 1. 改進語法,讓它可以處理更多的東西,例如數字。
> 1. 想想在游戲里你的`Sentence`類可以對用戶輸入做哪些有趣的事情。
## 常見問題
### Q: 我好像不能讓`assert_raises`正常運行
> 確認你寫的是`assert_raises(exception, callable, parameters)`而不是`assert_raises(exception, callable(parameters))`。注意一下第二種寫法中,調用了函數`callable`并將返回值傳遞給`assert_raises`,這種寫法是錯誤的,你應該把要調用的函數也作為參數傳遞給`assert_raises`。
- 序言
- 前言
- 簡介
- 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)
- 下一步