從這個小游戲的詞匯掃描器中,我們應(yīng)該可以得到類似下面的列表:
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')]
現(xiàn)在讓我們把它轉(zhuǎn)化成游戲可以使用的東西,也就是一個Sentence
類。
如果你還記得學(xué)校學(xué)過的東西的話,一個句子是由這樣的結(jié)構(gòu)組成的:主語(Subject) + 謂語(動詞 Verb) + 賓語(Object)
很顯然實際的句子可能會比這復(fù)雜,而你可能已經(jīng)在英語的語法課上面被折騰得夠嗆了。我們的目的,是將上面的元組列表轉(zhuǎn)換為一個Sentence
對象,而這個對象又包含主謂賓各個成員。
為了達到這個效果,你需要四(五)樣工具:
- 循環(huán)訪問元組列表的方法,這挺簡單的。
- 匹配我們的主謂賓設(shè)置中不同種類元組的方法。
- 一個“窺視”潛在元組的方法,以便做決定時用到。
- 跳過(skip)我們不在乎的內(nèi)容的方法,例如形容詞、冠詞等沒有用處的詞匯。
- 一個用來存放最終結(jié)果的
Sentence
對象
我們要把這些函數(shù)放在一個叫做ex48.parser
的類中,再把這個類放在ex48/parser.py
中,以便于我們能夠測試它們。我們使用peek
函數(shù)來查看元組列表中的下一個成員,做匹配以后再對它做下一步動作。
在你寫代碼之前,你要弄明白一個基礎(chǔ)的英語句子的語法是如何工作的。在我們的練習中,我們準備創(chuàng)建一個叫做Sentence
的類,它有如下3個屬性:
Sentence.subject(句子的主語)這是任意一個句子的主語,大部分時候可以默認為“玩家player”,比如一個句子“run north 向北跑”, 也就是說 "player run north 玩家向北跑"。主語應(yīng)該是一個名詞。
Sentence.verb(句子的謂語)這就是句子的的作用。 在 "run north" 中,謂語應(yīng)該是 "run". 謂語應(yīng)該是一個動詞。
Sentence.object(句子的賓語) 這又是一個名詞,指的是動詞做了什么。在我們游戲中, 我們分辨出的方向就是賓語。在 "run north" 中,單詞"north"就是賓語。在 "hit bear" 中,單詞"bear" 就是賓語。
我們的程序解析器使用我們給出的函數(shù)并返回解析后的句子,轉(zhuǎn)換成一個list
或Sentence
對象,用來接收匹配用戶輸入
已經(jīng)簡單學(xué)過關(guān)于異常的一些東西,但還沒學(xué)過怎樣拋出(raise)它們。這節(jié)的代碼演示了如何 raise 前面定義的ParserError
。注意ParserError
是一個定義為Exception
類型的 class。另外要注意我們是怎樣使用raise
這個關(guān)鍵字來拋出異常的。
你的測試代碼應(yīng)該也要測試到這些異常,這個我也會演示給你如何實現(xiàn)。
如果你希望更大的挑戰(zhàn),停在這里,然后只聽我的描述來完成代碼。當你遇到難題的時候,可以再回來看看是我如何做的。不過,嘗試自己實現(xiàn)代碼功能對你來說真的是個很好的鍛煉。我要開始串講我的代碼了,你可以開始在自己的ex48/parser.py
中輸入代碼。我們從異常處理開始我們的代碼編寫:
class ParserError(Exception):
pass
這就是你創(chuàng)建一個可以拋出的異常類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]
到目前為止,我們沒有寫什么特別的代碼,只是創(chuàng)建了兩個簡單的類。
在我們的問題描述中,我們需要一個函數(shù)用來看到列表中的單詞并返回單詞的類型:
def peek(word_list):
if word_list:
word = word_list[0]
return word[0]
else:
return None
我們需要這個函數(shù)是因為,我們要基于下一個單詞來選擇確認我們要處理的句子是什么,然后我們可以調(diào)用另一個函數(shù)來處理這個單詞,并將程序繼續(xù)下去。
我們使用match
函數(shù)來處理單詞,用它來確認預(yù)期中的單詞是否是正確的類型,將它移出列表,并返回該詞:
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
相當簡單是不是,不過還是要確認你理解了這些代碼以及為什么我是這么寫的。我需要依據(jù)我看到的列表中的下一個單詞來決定我現(xiàn)在處理的句子的類型,然后再用這個單詞創(chuàng)建我的Sentence
.
最后,我們需要一個方法來跳過句子中我們不關(guān)心的單詞。這些單詞會被打上“停用詞”(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”,經(jīng)過處理最后會得到"scream" 和 "bear".
以上是我們分析函數(shù)的基本結(jié)構(gòu),我可以用它們來處理我們需要的任何文本,盡管我們的程序非常簡單,剩下的函數(shù)也都是非常短的。
首先,我們來完成解析動詞的部分:
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"類的類似函數(shù):
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.")
重復(fù)操作,跳過"stop"類型的詞,提前判斷下一個詞,決定下一個"sentence".在函數(shù)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.")
所有的方式都準備好之后,我們最后一個函數(shù)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
這個函數(shù)來檢查異常,在 nose
的文檔里查看相關(guān)的內(nèi)容,學(xué)著使用它寫針對“執(zhí)行失敗”的測試,這也是測試很重要的一個方面。從nose
文檔中學(xué)會assert_raises
以及一些別的函數(shù)的使用方法。
寫完測試以后,你應(yīng)該就明白了這段程序的工作原理,而且也學(xué)會了如何為別人的程序?qū)憸y試代碼。 相信我,這是一個非常有用的技能。
- 修改
parse_
函數(shù)(方法),將它們放到一個類里邊,而不僅僅是獨立的方法函數(shù)。這兩種程序設(shè)計你喜歡哪一種呢?- 提高
parser
的容錯能力,這樣即使用戶輸入了你預(yù)定義語匯之外的詞語,你的程序也能正常運行下去。- 改進語法,讓它可以處理更多的東西,例如數(shù)字。
- 想想在游戲里你的
Sentence
類可以對用戶輸入做哪些有趣的事情。
assert_raises
正常運行確認你寫的是
assert_raises(exception, callable, parameters)
而不是assert_raises(exception, callable(parameters))
。注意一下第二種寫法中,調(diào)用了函數(shù)callable
并將返回值傳遞給assert_raises
,這種寫法是錯誤的,你應(yīng)該把要調(diào)用的函數(shù)也作為參數(shù)傳遞給assert_raises
。
更多建議: