# 14.5. `roman.py`, 第 5 阶段
现在 `fromRoman` 对于有效输入能够正常工作了,是揭开最后一个谜底的时候了:使它正常工作于无效输入的情况下。这意味着要找出一个方法检查一个字符串是不是有效的罗马数字。这比 `toRoman` 中[验证有效的数字输入](stage_3.html "14.3. roman.py, 第 3 阶段")困难,但是你可以使用一个强大的工具:正则表达式。
如果你不熟悉正则表达式,并且没有读过 [第 7 章 _正则表达式_](../regular_expressions/index.html "第 7 章 正则表达式"),现在是该好好读读的时候了。
如你在 [第 7.3 节 “个案研究:罗马字母”](../regular_expressions/roman_numerals.html "7.3. 个案研究:罗马字母")中所见到的,构建罗马数字有几个简单的规则:使用字母 `M`, `D`, `C`, `L`, `X`, `V` 和 `I`。让我们回顾一下:
1. 字符是被“加”在一起的:`I` 是 `1`,`II` 是 `2`,`III` 是 `3`。`VI` 是 `6` (看上去就是 “`5` 加 `1`”),`VII` 是 `7`,`VIII` 是 `8`。
2. 这些字符 (`I`, `X`, `C` 和 `M`) 最多可以重复三次。对于 `4`,你则需要利用下一个能够被5整除的字符进行减操作得到。你不能把 `4` 表示为 `IIII` 而应该表示为 `IV` (“比 `5` 小 `1` ”)。`40` 则被写作 `XL` (“比 `50` 小 `10`”),`41` 表示为 `XLI`,`42` 表示为 `XLII`,`43` 表示为 `XLIII`,`44` 表示为 `XLIV` (“比`50`小`10`,加上 `5` 小 `1`”)。
3. 类似地,对于数字 `9`,你必须利用下一个能够被10整除的字符进行减操作得到:`8` 是 `VIII`,而 `9` 是 `IX` (“比 `10` 小 `1`”),而不是 `VIIII` (由于 `I` 不能重复四次)。`90` 表示为 `XC`,`900` 表示为 `CM`。
4. 含五的字符不能被重复:`10` 应该表示为 `X`,而不会是 `VV`。`100` 应该表示为 `C`,而不是 `LL`。
5. 罗马数字一般从高位到低位书写,从左到右阅读,因此不同顺序的字符意义大不相同。`DC` 是 `600`,`CD` 是完全另外一个数 (`400`,“比 `500` 少 `100`”)。`CI` 是 `101`,而 `IC` 根本就不是一个有效的罗马数字 (因为你无法从`100`直接减`1`,应该写成 `XCIX`,意思是 “比 `100` 少 `10`,然后加上数字 `9`,也就是比 `10` 少 `1`”)。
## 例 14.12. `roman5.py`
这个程序可以在例子目录下的`py/roman/stage5/` 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 [下载本程序和其他样例程序](http://www.woodpecker.org.cn/diveintopython/download/diveintopython-exampleszh-cn-5.4b.zip "Download example scripts")。
```
"""Convert to and from Roman numerals"""
import re
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 4000):
raise OutOfRangeError, "number out of range (must be 1..3999)"
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
#Define pattern to detect valid Roman numerals
romanNumeralPattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
def fromRoman(s):
"""convert Roman numeral to integer"""
if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
```
| | |
| --- | --- |
| \[1\] | 这只是 [第 7.3 节 “个案研究:罗马字母”](../regular_expressions/roman_numerals.html "7.3. 个案研究:罗马字母") 中讨论的匹配模版的继续。十位上可能是`XC` (`90`),`XL` (`40`),或者可能是 `L` 后面跟着 0 到 3 个 `X` 字符。个位则可能是 `IX` (`9`),`IV` (`4`),或者是一个可能是 `V` 后面跟着 0 到 3 个 `I` 字符。 |
| \[2\] | 把所有的逻辑编码成正则表达式,检查无效罗马字符的代码就很简单了。如果 `re.search` 返回一个对象则表示匹配了正则表达式,输入是有效的,否则输入无效。 |
这里你可能会怀疑,这个面目可憎的正则表达式是否真能查出错误的罗马字符表示。没关系,不必完全听我的,不妨看看下面的结果:
## 例 14.13. 用 `romantest5.py` 测试 `roman5.py` 的结果
```
fromRoman should only accept uppercase input ... ok toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... ok fromRoman should fail with repeated pairs of numerals ... ok fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 12 tests in 2.864s
OK
```
| | |
| --- | --- |
| \[1\] | 有件事我未曾讲过,那就是默认情况下正则表达式大小写敏感。由于正则表达式 `romanNumeralPattern` 是以大写字母构造的,`re.search` 将拒绝不全部是大写字母构成的输入。因此大写输入的检查就通过了。 |
| \[2\] | 更重要的是,无效输入测试也通过了。例如,上面这个用例测试了 `MCMC` 之类的情形。正如你所见,这不匹配正则表达式,因此 `fromRoman` 引发一个测试用例正在等待的 `InvalidRomanNumeralError` 异常,所以测试通过了。 |
| \[3\] | 事实上,所有的无效输入测试都通过了。正则表达式捕捉了你在编写测试用例时所能预见的所有情况。 |
| \[4\] | 最终迎来了 “`OK`”这个平淡的“年度大奖”,所有测试都通过后 `unittest` 模块就会输出它。 |
> 注意
> 当所有测试都通过了,停止编程。
- 版权信息
- 第 1 章 安装 Python
- 1.1. 哪一种 Python 适合您?
- 1.2. Windows 上的 Python
- 1.3. Mac OS X 上的 Python
- 1.4. Mac OS 9 上的 Python
- 1.5. RedHat Linux 上的 Python
- 1.6. Debian GNU/Linux 上的 Python
- 1.7. 从源代码安装 Python
- 1.8. 使用 Python 的交互 Shell
- 1.9. 小结
- 第 2 章 第一个 Python 程序
- 2.1. 概览
- 2.2. 函数声明
- 2.3. 文档化函数
- 2.4. 万物皆对象
- 2.5. 代码缩进
- 2.6. 测试模块
- 第 3 章 内置数据类型
- 3.1. Dictionary 介绍
- 3.2. List 介绍
- 3.3. Tuple 介绍
- 3.4. 变量声明
- 3.5. 格式化字符串
- 3.6. 映射 list
- 3.7. 连接 list 与分割字符串
- 3.8. 小结
- 第 4 章 自省的威力
- 4.1. 概览
- 4.2. 使用可选参数和命名参数
- 4.3. 使用 type、str、dir 和其它内置函数
- 4.4. 通过 getattr 获取对象引用
- 4.5. 过滤列表
- 4.6. and 和 or 的特殊性质
- 4.7. 使用 lambda 函数
- 4.8. 全部放在一起
- 4.9. 小结
- 第 5 章 对象和面向对象
- 5.1. 概览
- 5.2. 使用 from _module_ import 导入模块
- 5.3. 类的定义
- 5.4. 类的实例化
- 5.5. 探索 UserDict:一个封装类
- 5.6. 专用类方法
- 5.7. 高级专用类方法
- 5.8. 类属性介绍
- 5.9. 私有函数
- 5.10. 小结
- 第 6 章 异常和文件处理
- 6.1. 异常处理
- 6.2. 与文件对象共事
- 6.3. for 循环
- 6.4. 使用 `sys.modules`
- 6.5. 与目录共事
- 6.6. 全部放在一起
- 6.7. 小结
- 第 7 章 正则表达式
- 7.1. 概览
- 7.2. 个案研究:街道地址
- 7.3. 个案研究:罗马字母
- 7.4. 使用 {n,m} 语法
- 7.5. 松散正则表达式
- 7.6. 个案研究:解析电话号码
- 7.7. 小结
- 第 8 章 HTML 处理
- 8.1. 概览
- 8.2. sgmllib.py 介绍
- 8.3. 从 HTML 文档中提取数据
- 8.4. BaseHTMLProcessor.py 介绍
- 8.5. locals 和 globals
- 8.6. 基于 dictionary 的字符串格式化
- 8.7. 给属性值加引号
- 8.8. dialect.py 介绍
- 8.9. 全部放在一起
- 8.10. 小结
- 第 9 章 XML 处理
- 9.1. 概览
- 9.2. 包
- 9.3. XML 解析
- 9.4. Unicode
- 9.5. 搜索元素
- 9.6. 访问元素属性
- 9.7. Segue [9]
- 第 10 章 脚本和流
- 10.1. 抽象输入源
- 10.2. 标准输入、输出和错误
- 10.3. 查询缓冲节点
- 10.4. 查找节点的直接子节点
- 10.5. 根据节点类型创建不同的处理器
- 10.6. 处理命令行参数
- 10.7. 全部放在一起
- 10.8. 小结
- 第 11 章 HTTP Web 服务
- 11.1. 概览
- 11.2. 避免通过 HTTP 重复地获取数据
- 11.3. HTTP 的特性
- 11.4. 调试 HTTP web 服务
- 11.5. 设置 User-Agent
- 11.6. 处理 Last-Modified 和 ETag
- 11.7. 处理重定向
- 11.8. 处理压缩数据
- 11.9. 全部放在一起
- 11.10. 小结
- 第 12 章 SOAP Web 服务
- 12.1. 概览
- 12.2. 安装 SOAP 库
- 12.3. 步入 SOAP
- 12.4. SOAP 网络服务查错
- 12.5. WSDL 介绍
- 12.6. 以 WSDL 进行 SOAP 内省
- 12.7. 搜索 Google
- 12.8. SOAP 网络服务故障排除
- 12.9. 小结
- 第 13 章 单元测试
- 13.1. 罗马数字程序介绍 II
- 13.2. 深入
- 13.3. romantest.py 介绍
- 13.4. 正面测试 (Testing for success)
- 13.5. 负面测试 (Testing for failure)
- 13.6. 完备性检测 (Testing for sanity)
- 第 14 章 测试优先编程
- 14.1. roman.py, 第 1 阶段
- 14.2. roman.py, 第 2 阶段
- 14.3. roman.py, 第 3 阶段
- 14.4. roman.py, 第 4 阶段
- 14.5. roman.py, 第 5 阶段
- 第 15 章 重构
- 15.1. 处理 bugs
- 15.2. 应对需求变化
- 15.3. 重构
- 15.4. 后记
- 15.5. 小结
- 第 16 章 函数编程
- 16.1. 概览
- 16.2. 找到路径
- 16.3. 重识列表过滤
- 16.4. 重识列表映射
- 16.5. 数据中心思想编程
- 16.6. 动态导入模块
- 16.7. 全部放在一起
- 16.8. 小结
- 第 17 章 动态函数
- 17.1. 概览
- 17.2. plural.py, 第 1 阶段
- 17.3. plural.py, 第 2 阶段
- 17.4. plural.py, 第 3 阶段
- 17.5. plural.py, 第 4 阶段
- 17.6. plural.py, 第 5 阶段
- 17.7. plural.py, 第 6 阶段
- 17.8. 小结
- 第 18 章 性能优化
- 18.1. 概览
- 18.2. 使用 timeit 模块
- 18.3. 优化正则表达式
- 18.4. 优化字典查找
- 18.5. 优化列表操作
- 18.6. 优化字符串操作
- 18.7. 小结
- 附录 A. 进一步阅读
- 附录 B. 五分钟回顾
- 附录 C. 技巧和窍门
- 附录 D. 示例清单
- 附录 E. 修订历史
- 附录 F. 关于本书
- 附录 G. GNU Free Documentation License
- G.0. Preamble
- G.1. Applicability and definitions
- G.2. Verbatim copying
- G.3. Copying in quantity
- G.4. Modifications
- G.5. Combining documents
- G.6. Collections of documents
- G.7. Aggregation with independent works
- G.8. Translation
- G.9. Termination
- G.10. Future revisions of this license
- G.11. How to use this License for your documents
- 附录 H. GNU 自由文档协议
- H.0. 序
- H.1. 适用范围和定义
- H.2. 原样复制
- H.3. 大量复制
- H.4. 修改
- H.5. 合并文档
- H.6. 文档合集
- H.7. 独立著作聚集
- H.8. 翻译
- H.9. 终止协议
- H.10. 协议将来的修订
- H.11. 如何为你的文档使用本协议
- 附录 I. Python license
- I.A. History of the software
- I.B. Terms and conditions for accessing or otherwise using Python
- 附录 J. Python 协议
- J.0. 关于译文的声明
- J.A. 软件的历史
- J.B. 使用 Python 的条款和条件