python 寫代碼語句

2021-09-15 14:57 更新

練習49.寫代碼語句

從這個小游戲的詞匯掃描器中,我們應(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 對象,而這個對象又包含主謂賓各個成員。

匹配(Match)和窺視(Peek)

為了達到這個效果,你需要四(五)樣工具:

  1. 循環(huán)訪問元組列表的方法,這挺簡單的。
  2. 匹配我們的主謂賓設(shè)置中不同種類元組的方法。
  3. 一個“窺視”潛在元組的方法,以便做決定時用到。
  4. 跳過(skip)我們不在乎的內(nèi)容的方法,例如形容詞、冠詞等沒有用處的詞匯。
  5. 一個用來存放最終結(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)換成一個listSentence對象,用來接收匹配用戶輸入

關(guān)于異常(Exception)

已經(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'

你應(yīng)該測試的東西

為《習題 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試代碼。 相信我,這是一個非常有用的技能。

附加題

  1. 修改 parse_函數(shù)(方法),將它們放到一個類里邊,而不僅僅是獨立的方法函數(shù)。這兩種程序設(shè)計你喜歡哪一種呢?
  2. 提高 parser的容錯能力,這樣即使用戶輸入了你預(yù)定義語匯之外的詞語,你的程序也能正常運行下去。
  3. 改進語法,讓它可以處理更多的東西,例如數(shù)字。
  4. 想想在游戲里你的Sentence類可以對用戶輸入做哪些有趣的事情。

常見問題

Q: 我好像不能讓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。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號