# 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 的條款和條件