创建句子
从这个小游戏的扫描器中(ex48),可得到类似下面的列表:
>>> from ex48 import lexicon
>>> print lexicon.scan("go north")
[('verb', 'go'), ('direction', 'north')]
>>> print lexicon.scan("kill the princess")
[('verb', 'kill'), ('stop', 'the'), ('noun', 'princess')]
>>> print lexicon.scan("eat the bear")
[('verb', 'eat'), ('stop', 'the'), ('noun', 'bear')]
>>> print lexicon.scan("open the door and smack the bear in the nose")
[('error', 'open'), ('stop', 'the'), ('noun', 'door'), ('error', 'and'), ('error', 'smack'), ('stop', 'the'), ('noun', 'bear'), ('stop', 'in'), ('stop', 'the'), ('error', 'nose')]
现在让我们把它转化为游戏可使用的东西,也就是一个语句类。
一个句子由以下结构组成:主语 谓语(动词) 宾语 。实际句子会复杂,但我们的目的是将上面的元组列表转换为一个语句对象—包含主、谓、宾的各个成员。
match和peek
要达到这个效果,需要下面四种工具:
1. 循环访问元组列表的方法,挺简单的
2. “匹配”(match)主谓宾设置中不同种类元组的方法
3. “窥视”(peek)潜在元组的方法, 以便做决定时用到
4. “跳过”(skip)我们不关心的内容的方法,如形容词、冠词等修饰词
把这些函数放到一个叫ex48/parser.py的文件以方便对其进行测试。使用peek函数来查看元组列表中的下一个成员,然后使用match函数取出一个元素对其进行操作。
peek函数:
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
skip函数:
def skip(word_list, word_type):
while peek(word_list) == word_type:
match(word_list, word_type)
句子的文法
有了工具,现在可从元组列表中来构建句子对象了。处理流程如下:
1. 使用peek识别下一个单词
2. 若这个单词和我们的语法匹配,就调用一个函数处理文法的这部分。假设函数的名字叫parse_subject
3. 如果语法不匹配,就产生一个错误,接下来你会学到这方面的内容
4. 全部分析完以后,应该能得到一个语句对象,然后可以将其应用在我们的游戏中
演示这个过程最简单的方法就是把代码展示给你,但这个习题有不一样的要求,我给出你程序来,你要为它写出测试代码来。
下面就是用来解析简单句子的代码,其使用了ex48.lexicon这个模块:
class ParserError(Exception):
pass
class Sentence(object):
def __init__(self, subject, verb, object):# remeber we take ('noun', 'princess') tuples and convert them
self.subject = subject[1]
self.verb = verb[1]
self.object = object[1]
def peek(word_list):if word_list:
word = word_list[0]
return word[0]
else:
return None
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
def skip(word_list, word_type):while peek(word_list) = word_type:
match(word_list, word_type)
def parse_verb(word_list):skip(word_list, 'stop')
if peek(word_list) == 'verb':return match(word_list, 'verb')
else:
raise ParserError("Expecting a verb next.")
def parse_object(word_list):skip(word_list, 'stop')
next = peek(word_list)
if next = 'noun':return match(word_list, 'noun')
if next = 'direction':
return match(word_list, 'direction')
else:
raise ParserError("Expected a noun or direction next.")
def parse_subject(word_list, subj):verb = parse_verb(word_list)
obj = parse_object(word_list)
return Sentence(subj, verb, obj)
def parse_sentence(word_list):skip(word_list, 'stop')
start = peek(word_list)
if start == 'noun':subj = match(word_list, 'noun')
return parse_subject(word_list, subj)
elif start == 'verb':
# assume the subject is the player then
return parse_subject(word_list, ('noun', 'player'))
else:
raise ParserError("Must start with subject, object, of verb not: %s" % start)
关于异常
你已简单学过关于异常的一些内容,但还没学过怎么引发它们。这个习题代码演示了如何用前面定义的ParserError来引发异常。注意ParserError是一个定义为Exception类型的类。另外要注意我们是怎样使用raise关键字来引发异常的。
你的测试代码也要测试到这些异常,这个我也会演示给你如何实现
应该测试的东西
为习49写一个完整的测试方案,确认代码中所有的东西都能正常工作。将这些测试放到tests/parser_tests.py中,与上一习题类似。其中包括异常测试——输入一个错误的句子它会抛出一个异常来。
使用assert_raises函数来检查异常,在nose文档中查看相关内容,学着用它写针对“执行失败”的测试,这也是测试很重要的一个方面。从nose文档中学会使用assert_raises以及其他一些函数。
写完测试以后,你应该就能明白这段程序的工作原理了,且学会了如何为别人的程序写测试代码——非常有用的技能!!
附加练习
1. 修改parse_方法,将他们放到一个类里边, 而不仅仅是只作为一个方法。这两种程序设计那你喜欢哪一种?
2. 提高怕热摄入对错误输入的抵御能力,这样即使用户输入了你预定义语汇之外的单词,你的程序也能正常运行。
3. 改进文法,让它可处理更多的东西,如数字
4. 想想在游戏里你可以使用这个语句类对用户输入做哪些有趣的事情
常见问题回答
assert_raises老是弄不对?
确认你写的是assert_raises(exception, callable, parameters)而不是assert_raises(exception, callable(parameters))。注意第二个格式所做的其实是调用这个函数,并将函数的返回值作为参数传给assert_raises, 这样做是错误的。必须把要调用的函数和它的参数分别传入assert_raises中。