練習(xí) 49. 創(chuàng)建句子
從我們這個(gè)小游戲的詞匯掃描器中窍仰,我們應(yīng)該可以得到類(lèi)似下面的列表:
Exercise 49 Python 會(huì)話(huà)
Python 3.6.0 (default, Feb 2 2017, 12:48:29)
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwi 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')]
以上對(duì)更長(zhǎng)的句子也管用鳖悠,比如:lexicon.scan("open the door and smack the bear in the nose")
宵统。
現(xiàn)在讓我們把這個(gè)轉(zhuǎn)換成游戲可以使用的東西蜒犯,比如句子類(lèi)(Sentence class)絮供。不知你是否還記得小學(xué)時(shí)候?qū)W過(guò)一個(gè)句子的簡(jiǎn)單結(jié)構(gòu):
主語(yǔ)(Subject) + 謂語(yǔ)(動(dòng)詞 Verb) + 賓語(yǔ)(Object)
顯然子巾,實(shí)際的句子比這個(gè)復(fù)雜帆赢,你可能已經(jīng)在英語(yǔ)語(yǔ)法課上被搞得頭大。我們的目的线梗,是將上面的元組列表轉(zhuǎn)換為一個(gè) Sentence 對(duì)象匿醒,而這個(gè)對(duì)象又包含主謂賓各個(gè)要素。
匹配和窺探(Peek)
為此我們需要四樣工具:
- 循環(huán)訪(fǎng)問(wèn)元組列表的方法缠导,這挺簡(jiǎn)單的廉羔。
- 匹配我們的主謂賓設(shè)置中不同種類(lèi)元組的方法。
- 一個(gè)“窺視”潛在元組的方法僻造,以便做決定時(shí)用到憋他。
- 跳過(guò)(skip)我們不在乎的內(nèi)容的方法,比如停用詞(stop word)髓削。
- 一個(gè)用以存放結(jié)果的句子類(lèi)竹挡。
我們要把這些函數(shù)放到一個(gè)叫做 ex48.parser
模塊中(將文該件命名為 ex48/parser.py
) ,以方便對(duì)其進(jìn)行測(cè)試立膛。我們使用 peek
函
數(shù)來(lái)執(zhí)行“查看元組列表中的下一個(gè)元素揪罕,然后做匹配梯码、取出來(lái)并進(jìn)行處理”這一系列動(dòng)作。
句子語(yǔ)法
在寫(xiě)代碼之前好啰,你需要先理解一下英語(yǔ)句子的基本語(yǔ)法轩娶。在我們的語(yǔ)法解析器(parser)中,我們想要產(chǎn)生一個(gè)包含三種屬性的句子對(duì)象:
Sentence.subject
這是任何句子的主語(yǔ)框往,但是大多數(shù)時(shí)候可以默認(rèn)為“玩家”(player)鳄抒,因?yàn)楸热纭皉un north”其實(shí)就是“player run north”。這應(yīng)該是一個(gè)名詞椰弊。
Sentence.verb
這是句子的動(dòng)作许溅。在“run north”中,就是“run”秉版。這是一個(gè)動(dòng)詞贤重。
Sentence.object
這是另一個(gè)名詞,指的是動(dòng)作所作用的對(duì)象(即賓語(yǔ)清焕,object)并蝗。在我們的游戲中,我們所分的方向就是賓語(yǔ)耐朴。所以在“run north”里面借卧,這個(gè)“north”就是賓語(yǔ)。在“hit bear”里面筛峭,“bear”就是賓語(yǔ)铐刘。
然后,我們的解析器需要使用我們所描述的函數(shù)影晓,給出的掃描過(guò)的句子镰吵,把它轉(zhuǎn)換成一列句子對(duì)象來(lái)和輸入內(nèi)容進(jìn)行匹配。
關(guān)于異常
你已經(jīng)簡(jiǎn)單學(xué)過(guò)一些關(guān)于異常的東西挂签,但還沒(méi)學(xué)過(guò)怎樣“拋出”(raise)異常疤祭。這節(jié)的代碼就演示了如何拋出前面定義的 ParserError
。注意饵婆,系統(tǒng)用類(lèi)來(lái)賦予異常的類(lèi)型勺馆。另外還要注意我們是如何使用 raise 這個(gè)關(guān)鍵字來(lái)拋出異常的。
你的測(cè)試代碼也應(yīng)該要測(cè)試到這些異常侨核,我隨后會(huì)演示給你看如何實(shí)現(xiàn)草穆。
解析器代碼(The Parser Code)
如果你想要額外的挑戰(zhàn),現(xiàn)在就停下來(lái)搓译,試著根據(jù)我的描述來(lái)寫(xiě)悲柱。如果遇到問(wèn)題,你可以回來(lái)看看我是如何做的些己,但是嘗試自己實(shí)現(xiàn)解析器是很好的實(shí)踐⊥慵Γ現(xiàn)在我會(huì)過(guò)一遍代碼嘿般,以便你可以將其輸入到 ex48/parser.py
中。我們以一個(gè)解析錯(cuò)誤異常來(lái)開(kāi)始我們的解析器:
parser.py
1 class ParserError(Exception):
2 pass
這也是你如何創(chuàng)建你自己的 ParserError
exception 類(lèi)的方法涯冠。下面炉奴,我們需要?jiǎng)?chuàng)建 Sentence object:
parser.py
1 class Sentence(object):
2
3 def __init__(self, subject, verb, obj):
4 # remember we take ('noun','princess') tuples and convert them.
5 self.subject = subject[1]
6 self.verb = verb[1]
7 self.object = obj[1]
這些代碼目前為止沒(méi)什么特別的。你只是在創(chuàng)建簡(jiǎn)單的類(lèi)功偿。
ai醬注: 接下來(lái)的這些函數(shù)不需要縮進(jìn)盆佣,它們不是 Sentence 類(lèi)下面的函數(shù)往堡,而是獨(dú)立的函數(shù)械荷!
在我們的問(wèn)題描述中,我們需要一個(gè)能夠“窺探”一列單詞并返回其類(lèi)型的函數(shù):
parser.py
1 def peek(word_list):
2 if word_list:
3 word = word_list[0]
4 return word[0]
5 else:
6 return None
我們之所以需要這個(gè)函數(shù)虑灰,是因?yàn)槲覀兊没谙乱粋€(gè)詞是什么來(lái)判斷我們正在處理的句子是什么類(lèi)型吨瞎。然后我們可以調(diào)用另一個(gè)函數(shù)來(lái)消滅(consume)那個(gè)字并往下進(jìn)行。
要消滅一個(gè)單詞穆咐,我們要用到 match
函數(shù)颤诀,這個(gè)函數(shù)可以確認(rèn)當(dāng)前單詞是不是正確的類(lèi)型,是的話(huà)就把它從列表中拿出來(lái)对湃,然后返回這個(gè)單詞崖叫。
parser.py
1 def match(word_list, expecting):
2 if word_list:
3 word = word_list.pop(0)
4
5 if word[0] == expecting:
6 return word
7 else:
8 return None
9 else:
10 return None
同樣的,這個(gè)也非常簡(jiǎn)單拍柒,但是你要確保你能理解這些代碼心傀。還要確保你能理解我為什么要用這種方式來(lái)實(shí)現(xiàn)它。我需要窺探列表中的單詞來(lái)決定我正在處理的句子是什么類(lèi)型拆讯,然后我需要匹配這些單詞來(lái)創(chuàng)建我的 Sentence脂男。
我需要的最后一個(gè)東西是跳過(guò)對(duì)句子無(wú)用的單詞的方法。這些單詞被標(biāo)記為“stop words” (type ’stop’) 种呐,比如“the”宰翅、“and”、和“a”等爽室。
parser.py
1 def skip(word_list, word_type):
2 while peek(word_list) == word_type:
3 match(word_list, word_type)
記住汁讼,skip 不只跳過(guò)一個(gè)單詞,它會(huì)跳過(guò)所有它所找到的那個(gè)類(lèi)型的單詞阔墩。比如嘿架,如果有人輸入 “scream at the bear”,你只會(huì)得到“scream”和“bear”這兩個(gè)詞戈擒。
這是我們解析函數(shù)的基本設(shè)定眶明,有了這個(gè)函數(shù),我們就可以解析任何我們想要解析的文本筐高。這個(gè)解析器非常簡(jiǎn)單搜囱,所以剩余的函數(shù)也很簡(jiǎn)短丑瞧。
首先,我們可以試著解析一個(gè)動(dòng)詞:
parser.py
1 def parse_verb(word_list):
2 skip(word_list, 'stop')
3
4 if peek(word_list) == 'verb':
5 return match(word_list, 'verb')
6 else:
7 raise ParserError("Expected a verb next.")
我們跳過(guò)了任何的 stop words蜀肘,然后提前進(jìn)行了窺探绊汹,確保下一個(gè)單詞是“verb”(動(dòng)詞)類(lèi)型。如果不是扮宠,就會(huì)拋出 ParserError
并說(shuō)明原因西乖。如果是“verb”,那就進(jìn)行匹配坛增,并把它從列表中拿出來(lái)获雕。處理賓語(yǔ)的函數(shù)同理:
parser.py
1 def parse_object(word_list):
2 skip(word_list, 'stop')
3 next_word = peek(word_list)
4
5 if next_word == 'noun':
6 return match(word_list, 'noun')
7 elif next_word == 'direction':
8 return match(word_list, 'direction')
9 else:
10 raise ParserError("Expected a noun or direction next.")
同樣地,跳過(guò) stop words收捣,先窺探届案,然后基于內(nèi)容決定句子是否正確。盡管在 parse_object
函數(shù)中罢艾,我們需要同時(shí)處理“noun”(名詞)和 “direction words”(方向詞)作為可能的賓語(yǔ)楣颠。主語(yǔ)也是一樣,但是因?yàn)槲覀兿胍秒[含的“player”名詞咐蚯,所以我們要這樣用 peek:
parser.py
1 def parse_subject(word_list):
2 skip(word_list, 'stop')
3 next_word = peek(word_list)
4
5 if next_word == 'noun':
6 return match(word_list, 'noun')
7 elif next_word == 'verb':
8 return ('noun', 'player')
9 else:
10 raise ParserError("Expected a verb next.")
這些都準(zhǔn)備好了以后童漩,我們最終的 parse_sentence
函數(shù)會(huì)非常簡(jiǎn)單:
parser.py
1 def parse_sentence(word_list):
2 subj = parse_subject(word_list)
3 verb = parse_verb(word_list)
4 obj = parse_object(word_list)
5
6 return Sentence(subj, verb, obj)
玩一玩解析器
要看這個(gè)如何運(yùn)行,你可以這樣做:
練習(xí) 49a Python 會(huì)話(huà)
Python 3.6.0 (default, Feb 2 2017, 12:48:29)
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwi Type "help", "copyright", "credits" or "license" for more informa
>>> 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'
ai醬注: 這里要先切換到 skeleton 目錄春锋,在運(yùn)行 python矫膨,因?yàn)橐肽K那里是從 ex48.parser 導(dǎo)入的,說(shuō)明不能在 ex48 這個(gè)目錄下運(yùn)行看疙。
試著把句子映射成句子中正確的對(duì)豆拨,比如,你會(huì)怎么說(shuō)“the bear run south”能庆?
你需要測(cè)試
對(duì)于練習(xí) 49施禾,編寫(xiě)一個(gè)完整的測(cè)試,以確認(rèn)代碼中的所有內(nèi)容都是有效的搁胆。把測(cè)試放在 tests/parser_tests.py
中弥搞,就像上個(gè)練習(xí)中的測(cè)試文件那樣。還要試著給解析器錯(cuò)誤的句子來(lái)產(chǎn)生異常渠旁。
通過(guò)使用 nose 文檔中的 assert_raise
函數(shù)來(lái)檢查異常攀例。學(xué)習(xí)如何使用它,這樣你就可以編寫(xiě)預(yù)期會(huì)失敗的測(cè)試顾腊,這在測(cè)試中是非常重要的粤铭。通過(guò)閱讀 nose 文檔來(lái)了解這個(gè)功能(以及其他功能)。
完成之后杂靶,你應(yīng)該知道這段代碼是如何工作的梆惯,以及如何為其他人的代碼寫(xiě)測(cè)試酱鸭,即使他們不希望你這樣做躯喇。相信我炼蹦,這是一個(gè)非常有用的技能。
附加練習(xí)
改變
parse_ methods
奶陈,試著把它們放到一個(gè)類(lèi)中怯屉,而不是只當(dāng)做方法來(lái)用蔚舀。你更喜歡哪種設(shè)計(jì)?提高 parser 對(duì)于錯(cuò)誤輸入的抵御能力锨络,這樣即使用戶(hù)輸入了你預(yù)定義語(yǔ)匯之外的詞語(yǔ)赌躺,你的程序也能正常運(yùn)行下去。
改進(jìn)語(yǔ)法足删,讓它可以處理更多的東西寿谴,例如數(shù)字锁右。
想想在游戲里你的 Sentence 類(lèi)可以對(duì)用戶(hù)輸入做哪些有趣的事情失受。
常見(jiàn)問(wèn)題
** assert_raises
老是弄不對(duì)。** 確認(rèn)你寫(xiě)成了 assert_raises(exception, callable, parameters)
而不
是 assert_raises(exception, callable(parameters))
咏瑟。注意第二個(gè)格式拂到,它所做的其實(shí)是將函數(shù)的返回值作為參數(shù)傳到 assert_raises
中,這樣做是錯(cuò)誤的码泞。你必須把函數(shù)和它的參數(shù)分別傳入 assert_raises
中兄旬。