练习 49 - 创建句子 - 笨办法学Python3

练习 49. 创建句子

从我们这个小游戏的词汇扫描器中,我们应该可以得到类似下面的列表:

Exercise 49 Python 会话

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')]

以上对更长的句子也管用,比如:lexicon.scan("open the door and smack the bear in the nose")

现在让我们把这个转换成游戏可以使用的东西,比如句子类(Sentence class)。不知你是否还记得小学时候学过一个句子的简单结构:

主语(Subject) + 谓语(动词 Verb) + 宾语(Object)

显然,实际的句子比这个复杂,你可能已经在英语语法课上被搞得头大。我们的目的,是将上面的元组列表转换为一个 Sentence 对象,而这个对象又包含主谓宾各个要素。

匹配和窥探(Peek)

为此我们需要四样工具:

  1. 循环访问元组列表的方法,这挺简单的。
  2. 匹配我们的主谓宾设置中不同种类元组的方法。
  3. 一个“窥视”潜在元组的方法,以便做决定时用到。
  4. 跳过(skip)我们不在乎的内容的方法,比如停用词(stop word)。
  5. 一个用以存放结果的句子类。

我们要把这些函数放到一个叫做 ex48.parser 模块中(将文该件命名为 ex48/parser.py) ,以方便对其进行测试。我们使用 peek
数来执行“查看元组列表中的下一个元素,然后做匹配、取出来并进行处理”这一系列动作。

句子语法

在写代码之前,你需要先理解一下英语句子的基本语法。在我们的语法解析器(parser)中,我们想要产生一个包含三种属性的句子对象:

Sentence.subject 这是任何句子的主语,但是大多数时候可以默认为“玩家”(player),因为比如“run north”其实就是“player run north”。这应该是一个名词。

Sentence.verb 这是句子的动作。在“run north”中,就是“run”。这是一个动词。

Sentence.object 这是另一个名词,指的是动作所作用的对象(即宾语,object)。在我们的游戏中,我们所分的方向就是宾语。所以在“run north”里面,这个“north”就是宾语。在“hit bear”里面,“bear”就是宾语。

然后,我们的解析器需要使用我们所描述的函数,给出的扫描过的句子,把它转换成一列句子对象来和输入内容进行匹配。

关于异常

你已经简单学过一些关于异常的东西,但还没学过怎样“抛出”(raise)异常。这节的代码就演示了如何抛出前面定义的 ParserError。注意,系统用类来赋予异常的类型。另外还要注意我们是如何使用 raise 这个关键字来抛出异常的。

你的测试代码也应该要测试到这些异常,我随后会演示给你看如何实现。

解析器代码(The Parser Code)

如果你想要额外的挑战,现在就停下来,试着根据我的描述来写。如果遇到问题,你可以回来看看我是如何做的,但是尝试自己实现解析器是很好的实践。现在我会过一遍代码,以便你可以将其输入到 ex48/parser.py 中。我们以一个解析错误异常来开始我们的解析器:

parser.py

1   class ParserError(Exception):
2       pass

这也是你如何创建你自己的 ParserError exception 类的方法。下面,我们需要创建 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]

这些代码目前为止没什么特别的。你只是在创建简单的类。

ai酱注: 接下来的这些函数不需要缩进,它们不是 Sentence 类下面的函数,而是独立的函数!

在我们的问题描述中,我们需要一个能够“窥探”一列单词并返回其类型的函数:

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

我们之所以需要这个函数,是因为我们得基于下一个词是什么来判断我们正在处理的句子是什么类型。然后我们可以调用另一个函数来消灭(consume)那个字并往下进行。

要消灭一个单词,我们要用到 match 函数,这个函数可以确认当前单词是不是正确的类型,是的话就把它从列表中拿出来,然后返回这个单词。

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

同样的,这个也非常简单,但是你要确保你能理解这些代码。还要确保你能理解我为什么要用这种方式来实现它。我需要窥探列表中的单词来决定我正在处理的句子是什么类型,然后我需要匹配这些单词来创建我的 Sentence。

我需要的最后一个东西是跳过对句子无用的单词的方法。这些单词被标记为“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 不只跳过一个单词,它会跳过所有它所找到的那个类型的单词。比如,如果有人输入 “scream at the bear”,你只会得到“scream”和“bear”这两个词。

这是我们解析函数的基本设定,有了这个函数,我们就可以解析任何我们想要解析的文本。这个解析器非常简单,所以剩余的函数也很简短。

首先,我们可以试着解析一个动词:

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.")

我们跳过了任何的 stop words,然后提前进行了窥探,确保下一个单词是“verb”(动词)类型。如果不是,就会抛出 ParserError 并说明原因。如果是“verb”,那就进行匹配,并把它从列表中拿出来。处理宾语的函数同理:

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.")

同样地,跳过 stop words,先窥探,然后基于内容决定句子是否正确。尽管在 parse_object 函数中,我们需要同时处理“noun”(名词)和 “direction words”(方向词)作为可能的宾语。主语也是一样,但是因为我们想要用隐含的“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.")

这些都准备好了以后,我们最终的 parse_sentence 函数会非常简单:

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)

玩一玩解析器

要看这个如何运行,你可以这样做:

练习 49a Python 会话

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 目录,在运行 python,因为引入模块那里是从 ex48.parser 导入的,说明不能在 ex48 这个目录下运行。

试着把句子映射成句子中正确的对,比如,你会怎么说“the bear run south”?

你需要测试

对于练习 49,编写一个完整的测试,以确认代码中的所有内容都是有效的。把测试放在 tests/parser_tests.py 中,就像上个练习中的测试文件那样。还要试着给解析器错误的句子来产生异常。

通过使用 nose 文档中的 assert_raise 函数来检查异常。学习如何使用它,这样你就可以编写预期会失败的测试,这在测试中是非常重要的。通过阅读 nose 文档来了解这个功能(以及其他功能)。

完成之后,你应该知道这段代码是如何工作的,以及如何为其他人的代码写测试,即使他们不希望你这样做。相信我,这是一个非常有用的技能。

附加练习

  1. 改变 parse_ methods,试着把它们放到一个类中,而不是只当做方法来用。你更喜欢哪种设计?

  2. 提高 parser 对于错误输入的抵御能力,这样即使用户输入了你预定义语汇之外的词语,你的程序也能正常运行下去。

  3. 改进语法,让它可以处理更多的东西,例如数字。

  4. 想想在游戏里你的 Sentence 类可以对用户输入做哪些有趣的事情。

常见问题

** assert_raises 老是弄不对。** 确认你写成了 assert_raises(exception, callable, parameters) 而不
assert_raises(exception, callable(parameters)) 。注意第二个格式,它所做的其实是将函数的返回值作为参数传到 assert_raises 中,这样做是错误的。你必须把函数和它的参数分别传入 assert_raises 中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容