Exercise 49 Making Sentence的实现及理解
在ex49 Making Sentence, 即构造句子这一练习中,作者提供了程序源码,要求读者写出对应的测试单元。同时,在进阶练习单元,作者建议将做好的单元测试函数,改写至类中,下面提供个人的实现方法,并对编写过程中遇到的部分代码原理,作了测试,以验证部分猜想。
作者提供的源码及个人理解
# file: .\ex48\ex48\parser.py
class ParserError(Exception):
pass
class Sentence(object):
def __init__(self, subj, verb, obj):
self.subj = subj[1]
self.verb = verb[1]
self.obj = obj[1]
def get_type(word_list):
# 读取的类型,如:返回('nouns', 'door')中的'nouns'
if word_list:
word = word_list[0]
#从元组构成的列表中取出第一个元组元素。
return word[0]
#从元组元素中返回第一个字符串
else:
return None
def match(word_list, expecting):
# 传入由多个元组构成的列表,查看第1个元组是不是expect想找的元组
# 如果是,取走该元组。不是,则删除该元组
if word_list:
word = word_list.pop(0)
# 读取列表中的第1个元素,这个元素是一个元组,如('nouns', 'door')。
if word[0] == expecting:
return word
# 本例中,元组第1元素表示的是类型,
# 如果这个类型是我们要找的类型,就把这个元组返回
else:
return None
# 不管找没找到,都使word_list元素减少了1个
else:
return "list Null"
def skip(word_list, word_type):
# 从word_list中跳过word_type类型的元组,直到list的第1个元素不是word_type
while get_type(word_list) == word_type:
match(word_list, word_type)
# 因为match总会pop掉一个元素,才使得这个函数得以成立
# 换成pop(word_list.pop(0))更好
# 但我感觉函数之前的相互依赖性又太强了。match做了匹配之外更多的事情。
def parse_verb(word_list):
# 先跳过stop类的词组(无用但合法),考察第1个元素是不是动词
# 如果是,则取走;如果不是,不取走,报错。
skip(word_list, 'stop')
word_type = get_type(word_list)
if word_type == 'verb':
return match(word_list, 'verb')
# 找到就取走
else:
raise ParserError("Expected a verb next.")
def parse_object(word_list):
skip(word_list, 'stop')
word_type = get_type(word_list)
if word_type == 'noun':
return match(word_list, 'noun')
elif word_type == 'direction':
return match(word_list, 'direction')
else:
raise ParserError("Expected a noun or direction next.")
def parse_subject(word_list):
skip(word_list, 'stop')
word_type = get_type(word_list)
if word_type == 'noun':
return match(word_list, 'noun')
elif word_type == 'verb':
return ('noun', 'player')
# 当第1个词是动词是,默认地认为主语为player,也没有取走第1个元素
# 这是合情合理的。
else:
raise ParserError("Expected a verb next.")
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)
源码的单元测试
# file: .\ex48\test\parser_test.py
from nose.tools import *
from ex48.parser import *
#import的写法与文件存放位置息息相关
def test_get_type():
test_list1 = [('verb', 'open'), ]
assert_equal(get_type(test_list1), 'verb')
test_list2 = [('', 'kill'), ('verb', 'open')]
assert_equal(get_type(test_list2), '')
test_list3 = [(None, None), ]
assert_equal(get_type(test_list3), None)
def test_match():
test_list1 = [
('verb', 'open'),
('stop', 'the'),
('noun', 'door'),
]
assert_equal(match(test_list1, 'verb'),
('verb', 'open'))
assert_equal(match(test_list1, 'stop'),
('stop', 'the'))
test_list2 = [(None, None), ]
assert_equal(match(test_list2, 123),
None)
assert_equal(test_list2, [])
assert_equal(match(test_list2, 123),
"list Null")
def test_skip():
test_list1 = [
('verb', 'open'),
('stop', 'the'),
('noun', 'door'),
]
skip(test_list1, 'verb')
assert_equal(test_list1[0], ('stop', 'the'))
assert_equal(test_list1[0][0], 'stop')
assert_equal(test_list1[0][0][0], 's')
# 这里,我顺带测试了test_list1的第一层内部元素。很棒。
test_list2 = [
('1', 'AAAA'),
('1', 'tDFWERhe'),
('noun', 'door'),
]
skip(test_list2, '1')
assert_equal(test_list2, [('noun', 'door'),])
# 最后一个逗号加不加都一样。
test_list3 = [
('2', 'AAAA'),
('1', 'tDFWERhe'),
('noun', 'door'),
]
skip(test_list3, '1')
assert_equal(test_list3[0], ('2', 'AAAA'))
def test_parse_verb():
test_list1 = [
('stop', 'the'),
('verb', 'open'),
('noun', 'door'),
]
assert_equal(parse_verb(test_list1),
('verb', 'open'))
assert_raises(ParserError, parse_verb, test_list1)
# **本句含义为:让系统判断,parse_verb(test_list1)执行后
# 是不是返回ParserError错误。
# assert_raises参数详解:
# 1参数:错误的类型,如本例中为片定义的ParserError
# 2参数:你预计会出错的那个函数名。本例中,test_list1
# 只剩下('noun', 'door')这一元素,按理运行parse_verb
# 后肯定报错,所以将parse_verb放在2参数。注意别加括号!
# 3、4、5参数:填入“预计会出错的那个函数,所需要的参数”。
# 可拓展,与2参数实际需要的函数对应。
# 现在你可以看懂上面标**的那句解释了。
test_list2 = [
('noun', 'door'),
('a', 'b')
]
assert_raises(ParserError, parse_verb, test_list2)
def test_parse_object():
test_list1 = [
('noun', 'door'),
('direction', 'b')
]
assert_equal(parse_object(test_list1), ('noun', 'door'))
assert_equal(parse_object(test_list1), ('direction', 'b'))
assert_raises(ParserError, parse_object, test_list1)
def test_parse_subject():
test_list1 = [
('stop', 'the'),
('verb', 'open'),
('noun', 'door'),
]
assert_equal(parse_subject(test_list1), ('noun', 'player'))
parse_verb(test_list1)
assert_equal(parse_subject(test_list1), ('noun', 'door'))
assert_raises(ParserError, parse_subject, test_list1)
def test_parse_sentence():
test_list1 = [
('stop', 'the'),
('verb', 'open'),
('noun', 'door'),
]
sentence = parse_sentence(test_list1)
assert_equal(sentence.subj, 'player')
assert_equal(sentence.verb, 'open')
assert_equal(sentence.obj, 'door')
test_list2 = [
('verb', 'kill'),
('noun', 'you'),
('stop', 'and'),
('verb', 'eat'),
('noun', 'bear'),
]
sentence2 = parse_sentence(test_list2)
assert_equal(sentence2.subj, 'player')
assert_equal(sentence2.verb, 'kill')
assert_equal(sentence2.obj, 'you')
sentence3 = parse_sentence(test_list2)
assert_equal(sentence3.subj, 'player')
assert_equal(sentence3.obj, 'bear')
# finish!
进阶练习改写后的源码
# file: .\ex48\ex48\parser_by_class.py
class ParserError(Exception):
pass
class Sentence(object):
def __init__(self, subj, verb, obj):
self.subj = subj[1]
self.verb = verb[1]
self.obj = obj[1]
class Parse(object):
def __init__(self, word_list):
# self.sentence = Sentence()
self.word_list = word_list
def get_type(self):
# 读取的类型,如:返回('nouns', 'door')中的'nouns'
if self.word_list:
word = self.word_list[0]
#从元组构成的列表中取出第一个元组元素。
return word[0]
#从元组元素中返回第一个字符串
else:
return None
def match(self, expecting):
# 传入由多个元组构成的列表,查看第1个元组是不是expect想找的元组
# 如果是,取走该元组。不是,则删除该元组
if self.word_list:
word = self.word_list.pop(0)
# 读取列表中的第1个元素,这个元素是一个元组,如('nouns', 'door')。
if word[0] == expecting:
return word
# 本例中,元组第1元素表示的是类型,
# 如果这个类型是我们要找的类型,就把这个元组返回
else:
return None
# 不管找没找到,都使word_list元素减少了1个
else:
return "list Null"
def skip(self, word_type):
# 从word_list中跳过word_type类型的元组,直到list的第1个元素不是word_type
while self.get_type() == word_type:
self.match(word_type)
# 因为match总会pop掉一个元素,才使得这个函数得以成立
# 换成pop(word_list.pop(0))更好
# 但我感觉函数之前的相互依赖性又太强了。match做了匹配之外更多的事情。
def get_verb(self):
# 先跳过stop类的词组(无用但合法),考察第1个元素是不是动词
# 如果是,则取走;如果不是,不取走,报错。
self.skip('stop')
word_type = self.get_type()
if word_type == 'verb':
return self.match('verb')
# 找到就取走
else:
raise ParserError("Expected a verb next.")
def get_object(self):
self.skip('stop')
word_type = self.get_type()
if word_type == 'noun':
return self.match('noun')
elif word_type == 'direction':
return self.match('direction')
else:
raise ParserError("Expected a noun or direction next.")
def get_subject(self):
self.skip('stop')
word_type = self.get_type()
if word_type == 'noun':
return self.match('noun')
elif word_type == 'verb':
return ('noun', 'player')
# 当第1个词是动词是,默认地认为主语为player,也没有取走第1个元素
# 这是合情合理的。
else:
raise ParserError("Expected a verb next.")
def get_sentence(self):
subj = self.get_subject()
verb = self.get_verb()
obj = self.get_object()
return Sentence(subj, verb, obj)
进阶练习对应的单元测试
from nose.tools import *
from ex48.parser_by_class import *
def test_get_type():
test_list1 = [('verb', 'open'), ]
list1 = Parse(test_list1)
assert_equal(list1.get_type(), 'verb')
test_list2 = [('', 'kill'), ('verb', 'open')]
list2 = Parse(test_list2)
assert_equal(list2.get_type(), '')
test_list3 = [(None, None), ]
list3 = Parse(test_list3)
assert_equal(list3.get_type(), None)
def test_match():
test_list1 = [
('verb', 'open'),
('stop', 'the'),
('noun', 'door'),
]
list1 = Parse(test_list1)
assert_equal(list1.match('verb'),
('verb', 'open'))
assert_equal(list1.match('stop'),
('stop', 'the'))
test_list2 = [(None, None), ]
list2 = Parse(test_list2)
assert_equal(list2.match(123),
None)
assert_equal(list2.word_list, [])
assert_equal(list2.match(123),
"list Null")
def test_skip():
test_list1 = [
('verb', 'open'),
('stop', 'the'),
('noun', 'door'),
]
list1 = Parse(test_list1)
list1.skip('verb')
assert_equal(test_list1[0], ('stop', 'the'))
assert_equal(test_list1[0][0], 'stop')
assert_equal(test_list1[0][0][0], 's')
# 这里,我顺带测试了test_list1的第一层内部元素。很棒。
test_list2 = [
('1', 'AAAA'),
('1', 'tDFWERhe'),
('noun', 'door'),
]
list2 = Parse(test_list2)
list2.skip('1')
assert_equal(test_list2, [('noun', 'door'),])
# 最后一个逗号加不加都一样。
test_list3 = [
('2', 'AAAA'),
('1', 'tDFWERhe'),
('noun', 'door'),
]
list3 = Parse(test_list3)
list3.skip('1')
assert_equal(test_list3[0], ('2', 'AAAA'))
assert_equal(list3.word_list[0], ('2', 'AAAA'))
def test_get_verb():
test_list1 = [
('stop', 'the'),
('verb', 'open'),
('noun', 'door'),
]
list1 = Parse(test_list1)
assert_equal(list1.get_verb(),
('verb', 'open'))
assert_raises(ParserError, list1.get_verb)
test_list2 = [
('noun', 'door'),
('a', 'b')
]
list2 = Parse(test_list2)
assert_raises(ParserError, list2.get_verb)
def test_get_object():
test_list1 = [
('noun', 'door'),
('direction', 'b')
]
list1 = Parse(test_list1)
assert_equal(list1.get_object(), ('noun', 'door'))
assert_equal(list1.get_object(), ('direction', 'b'))
assert_raises(ParserError, list1.get_object)
def test_get_subject():
test_list1 = [
('stop', 'the'),
('verb', 'open'),
('noun', 'door'),
]
list1 = Parse(test_list1)
assert_equal(list1.get_subject(), ('noun', 'player'))
list1.get_verb()
assert_equal(list1.get_subject(), ('noun', 'door'))
assert_raises(ParserError, list1.get_subject)
def test_get_sentence():
test_list1 = [
('stop', 'the'),
('verb', 'open'),
('noun', 'door'),
]
list1 = Parse(test_list1)
# sentence = test_list1.get_sentence
sentence = list1.get_sentence()
assert_equal(sentence.subj, 'player')
assert_equal(sentence.verb, 'open')
assert_equal(sentence.obj, 'door')
test_list2 = [
('verb', 'kill'),
('noun', 'you'),
('stop', 'and'),
('verb', 'eat'),
('noun', 'bear'),
]
list2 = Parse(test_list2)
sentence2 = list2.get_sentence()
assert_equal(sentence2.subj, 'player')
assert_equal(sentence2.verb, 'kill')
assert_equal(sentence2.obj, 'you')
# 下面针对list2 继续操作。
sentence3 = list2.get_sentence()
assert_equal(sentence3.subj, 'player')
assert_equal(sentence3.obj, 'bear')
# finish!
总结
从进阶练习来看,以面向对象的思想,改写为class后,有以下改变:
- 函数在引用时的语法,更接近自然语言语法。如:
调用原来的取动词函数parse_verb(list1),改写后,用list.get_verb()来调用,使得程序可读性更高。 - 实现class以后,即"封装"后,使编写者的思维更加结构化。如:
在对list1进行操作时,编写者只能将parser.py作为一个函数库,随时记忆这个库里应当采用什么函数对list1进行处理。
改写后,list1作为一个类的实例,list1就变成一个箱子,这个箱子里虽然也算作一个函数库,但当你处理list2以后,list1里面有什么函数,就不用关心了。
即,你的不必将整个parser.py函数库装载进大脑,以便于编程,而可以将每个实例(每个list)看作一个盒子,你要对盒子操作时,装载盒子里面有的函数,就可以了。私以为,这实实在在地减少了大脑负荷。 - 可以控制函数的作用域。这与第2点有一定的内在关系。如:
这里写的skip函数本意,仅对(‘noun’, ‘door’)一样的数据作以处理。所以当不处理此类数据时,编程者的大脑里就不必记忆这些函数,系统也不需要使用它们了。
顺理成章地,将skip封闭入类以后,它仅由“盒子”的内部调用,编程者在编写时,对这个函数的使用也更放心,因为不需要担心它在类以外的地方出现,系统在背后帮你做完了一切。
进一步地,在类之外,万一有需求,我们还能编写一个skip函数,新旧skip函数,互不相扰,相得益彰。 - 还能暴露原来函数命名的缺点。
书本作者编写的本程序,在内部是自洽的,但作为一个盒子对外展示时,容易使调用者产生误解。如:
word_type = get_type(word_list)这一句,是容易理解的,但其背后隐藏了1个信息:只取得word_list第1个元素的类型。改写为类时,我将这一句是改成了:word_type = self.get_type()。单看改写后的句子,似乎是将类内部的变量类型取得。这就与get_type实际作用相矛盾。
矛盾的出现,就是缺点的暴露。
解决这一矛盾,却也简单,将get_type、match等函数,改为get_1st_word_type、match_1st_word等即可;又或者,u将Parse类,改名为Parse_1st_word。