# 习题 48: 更复杂的用户输入
你的游戏可能一路跑得很爽,不过你处理用户输入的方式肯定让你不胜其烦了。每一个房间都需要一套自己的语句,而且只有用户完全输入正确后才能执行。你需要一个设备,它可以允许用户以各种方式输入语汇。例如下面的机种表述都应该被支持才对:
- open door
- open the door
- go THROUGH the door
- punch bear
- Punch The Bear in the FACE
也就是说,如果用户的输入和常用英语很接近也应该是可以的,而你的游戏要识别出它们的意思。为了达到这个目的,我们将写一个模组专门做这件事情。这个模组里边会有若干个类,它们互相配合,接受用户输入,并且将用户输入转换成你的游戏可以识别的命令。
英语的简单格式是这个样子的:
- 单词由空格隔开。
- 句子由单词组成。
- 语法控制句子的含义。
所以最好的开始方式是先搞定如何得到用户输入的词汇,并且判断出它们是什么。
### 我们的游戏语汇
我在游戏里创建了下面这些语汇:
- 表示方向: north, south, east, west, down, up, left, right, back.
- 动词: go, stop, kill, eat.
- 修饰词: the, in, of, from, at, it
- 名词: door, bear, princess, cabinet.
- 数词: 由 0-9 构成的数字。
说到名词,我们会碰到一个小问题,那就是不一样的房间会用到不一样的一组名词,不过让我们先挑一小组出来写程序,以后再做改进把。
### 如何断句
我们已经有了词汇表,为了分析句子的意思,接下来我们需要找到一个断句的方法。我们对于句子的定义是“空格隔开的单词”,所以只要这样就可以了:
~~~
stuff = raw_input('> ')
words = stuff.split()
~~~
目前做到这样就可以了,不过这招在相当一段时间内都不会有问题。
### 语汇元组
一旦我们知道了如何将句子转化成词汇列表,剩下的就是逐一检查这些词汇,看它们是什么类型。为了达到这个目的,我们将用到一个非常好使的 Python 数据结构,叫做”元组(tuple)”。元组其实就是一个不能修改的列表。创建它的方法和创建列表差不多,成员之间需要用逗号隔开,不过方括号要换成圆括号 () :
~~~
first_word = ('direction', 'north')
second_word = ('verb', 'go')
sentence = [first_word, second_word]
~~~
这样我们就创建了一个 (TYPE, WORD) 组,让你识别出单词,并且对它执行指令。
这只是一个例子,不过最后做出来的样子也差不多。你接受用户输入,用 split 将其分隔成单词列表,然后分析这些单词,识别它们的类型,最后重新组成一个句子。
### 扫描输入
现在你要写的是词汇扫描器。这个扫描器会将用户的输入字符串当做参数,然后返回由多个 (TOKEN, WORD) 组成的一个列表,这个列表实现类似句子的功能。如果一个单词不在预定的词汇表中,那它返回时 WORD 应该还在,但 TOKEN 应该设置成一个专门的错误标记。这个错误标记将告诉用户哪里出错了。
有趣的地方来了。我不会告诉你这些该怎样做,但我会写一个“单元测试(unit test)”,而你要把扫描器写出来,并保证单元测试能够正常通过。
### “异常”和数字
有一件小事情我会先帮帮你,那就是数字转换。为了做到这一点,我们会作一点弊,使用“异常(exceptions)”来做。“异常”指的是你运行某个函数时得到的错误。你的函数在碰到错误时,就会“提出(raise)”一个“异常”,然后你就要去处理(handle)这个异常。假如你在Python 里写了这些东西:
~~~
~/projects/simplegame $ python
Python 2.6.5 (r265:79063, Apr 16 2010, 13:57:41)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> int("hell")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'hell'
>>
~~~
这个 ValueError 就是 int() 函数抛出的一个异常。因为你给 int() 的参数不是一个数字。 int() 函数其实也可以返回一个值来告诉你它碰到了错误,不过由于它只能返回整数值,所以很难做到这一点。它不能返回 -1,因为这也是一个数字。 int() 没有纠结在它“究竟应该返回什么”上面,而是提出了一个叫做 ValueError 的异常,然后你只要处理这个异常就可以了。
处理异常的方法是使用 try 和 except 这两个关键字:
~~~
def convert_number(s):
try:
return int(s)
except ValueError:
return None
~~~
你把要试着运行的代码放到 try 的区段里,再将出错后要运行的代码放到 except 区段里。在这里,我们要试着调用 int() 去处理某个可能是数字的东西,如果中间出了错,我们就抓到这个错误,然后返回 None。
在你写的扫描器里面,你应该使用这个函数来测试某个东西是不是数字。做完这个检查,你就可以声明这个单词是一个错误单词了。
### 你应该测试的东西
这里是你应该使用的测试文件 tests/lexicon_tests.py :
<table class="highlighttable"><tbody><tr><td class="linenos"> <div class="linenodiv"> <pre> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46</pre> </div> </td> <td class="code"> <div class="highlight"> <pre>from nose.tools import *
from ex48 import lexicon
def test_directions():
assert_equal(lexicon.scan("north"), [('direction', 'north')])
result = lexicon.scan("north south east")
assert_equal(result, [('direction', 'north'),
('direction', 'south'),
('direction', 'east')])
def test_verbs():
assert_equal(lexicon.scan("go"), [('verb', 'go')])
result = lexicon.scan("go kill eat")
assert_equal(result, [('verb', 'go'),
('verb', 'kill'),
('verb', 'eat')])
def test_stops():
assert_equal(lexicon.scan("the"), [('stop', 'the')])
result = lexicon.scan("the in of")
assert_equal(result, [('stop', 'the'),
('stop', 'in'),
('stop', 'of')])
def test_nouns():
assert_equal(lexicon.scan("bear"), [('noun', 'bear')])
result = lexicon.scan("bear princess")
assert_equal(result, [('noun', 'bear'),
('noun', 'princess')])
def test_numbers():
assert_equal(lexicon.scan("1234"), [('number', 1234)])
result = lexicon.scan("3 91234")
assert_equal(result, [('number', 3),
('number', 91234)])
def test_errors():
assert_equal(lexicon.scan("ASDFADFASDF"), [('error', 'ASDFADFASDF')])
result = lexicon.scan("bear IAS princess")
assert_equal(result, [('noun', 'bear'),
('error', 'IAS'),
('noun', 'princess')])
</pre> </div> </td> </tr></tbody></table>
记住你要使用你的项目骨架来创建新项目,将这个测试用例写下来(不许复制粘贴!),然后编写你的扫描器,直至所有的测试都能通过。注意细节并确认结果一切工作良好。
### 设计的技巧
集中一次实现一个测试项目,尽量保持项目简单,只要把你的 lexicon.py 词汇表中所有的单词放那里就可以了。不要修改输入的单词表,不过你需要创建自己的新列表,里边包含你的语汇元组。另外,记得使用 in 关键字来检查这些语汇列表,以确认某个单词是否在你的语汇表中。
### 加分习题
1. 改进单元测试,让它覆盖到更多的语汇。
1. 向语汇列表添加更多的语汇,并且更新单元测试代码。
1. 让你的扫描器能够识别任意大小写的词汇。更新你的单元测试以确认其功能。
1. 找出另外一种转换为数字的方法。
1. 我的解决方案用了 37 行代码,你的是更长还是更短呢?
- 译者前言
- 前言:笨办法更简单
- 习题 0: 准备工作
- 习题 1: 第一个程序
- 习题 2: 注释和井号
- 习题 3: 数字和数学计算
- 习题 4: 变量(variable)和命名
- 习题 5: 更多的变量和打印
- 习题 6: 字符串(string)和文本
- 习题 7: 更多打印
- 习题 8: 打印,打印
- 习题 9: 打印,打印,打印
- 习题 10: 那是什么?
- 习题 11: 提问
- 习题 12: 提示别人
- 习题 13: 参数、解包、变量
- 习题 14: 提示和传递
- 习题 15: 读取文件
- 习题 16: 读写文件
- 习题 17: 更多文件操作
- 习题 18: 命名、变量、代码、函数
- 习题 19: 函数和变量
- 习题 20: 函数和文件
- 习题 21: 函数可以返回东西
- 习题 22: 到现在你学到了哪些东西?
- 习题 23: 读代码
- 习题 24: 更多练习
- 习题 25: 更多更多的练习
- 习题 26: 恭喜你,现在可以考试了!
- 习题 27: 记住逻辑关系
- 习题 28: 布尔表达式练习
- 习题 29: 如果(if)
- 习题 30: Else 和 If
- 习题 31: 作出决定
- 习题 32: 循环和列表
- 习题 33: While 循环
- 习题 34: 访问列表的元素
- 习题 35: 分支和函数
- 习题 36: 设计和调试
- 习题 37: 复习各种符号
- 习题 38: 阅读代码
- 习题 39: 列表的操作
- 习题 40: 字典, 可爱的字典
- 习题 41: 来自 Percal 25 号行星的哥顿人(Gothons)
- 习题 42: 物以类聚
- 习题 43: 你来制作一个游戏
- 习题 44: 给你的游戏打分
- 习题 45: 对象、类、以及从属关系
- 习题 46: 一个项目骨架
- 习题 47: 自动化测试
- 习题 48: 更复杂的用户输入
- 习题 49: 创建句子
- 习题 50: 你的第一个网站
- 习题 51: 从浏览器中获取输入
- 习题 52: 创建你的 web 游戏
- 下一步
- 老程序员的建议