當前位置: 首頁 ? 深入 Python 3 ?
難度級別: ?????
? Certitude is not the test of certainty. We have been cocksure of many things that were not so. ?
— Oliver Wendell Holmes, Jr.
在此章節中,你將要編寫及調試一系列用于阿拉伯數字與羅馬數字相互轉換的方法。你閱讀了在“案例學習:羅馬數字”中關于構建及校驗羅馬數字的機制。那么,現在考慮擴展該機制為一個雙向的方法。
羅馬數字的規則引出很多有意思的結果:
1000。為了達到本節的目的,限定羅馬數字在 1 到 3999 之間。現在,開始設計 roman.py 模塊。它有兩個主要的方法:to_roman() 及 from_roman()。to_roman() 方法接收一個從 1 到 3999 之間的整型數字,然后返回一個字符串類型的羅馬數字。
在這里停下來。現在讓我們進行一些意想不到的操作:編寫一個測試用例來檢測 to_roman 函數是否實現了你想要的功能。你想得沒錯:你正在編寫測試尚未編寫代碼的代碼。
這就是所謂的測試驅動開發 或 TDD。那兩個轉換方法( to_roman() 及之后的 from_roman())可以獨立于任何使用它們的大程序而作為一個單元來被編寫及測試。Python 自帶一個單元測試框架,被恰當地命名為 unittest 模塊。
單元測試是整個以測試為中心的開發策略中的一個重要部分。編寫單元測試應該安排在項目的早期,同時要讓它隨同代碼及需求變更一起更新。很多人都堅持測試代碼應該先于被測試代碼的,而這種風格也是我在本節中所主張的。但是,不管你何時編寫,單元測試都是有好處的。
?
一個測試用例僅回答一個關于它正在測試的代碼問題。一個測試用例應該可以:
讓我們據此為第一個需求建立一個測試用例:
to_roman() 方法應該返回代表1-3999的羅馬數字。這些代碼功效如何并不那么顯而易見。它定義了一個沒有__init__ 方法的類。而該類當然有其它方法,但是這些方法都不會被調用。在整個腳本中,有一個__main__ 塊,但它并不引用該類及它的方法。但我承諾,它做別的事情了。
import roman1
import unittest
class KnownValues(unittest.TestCase): ①
known_values = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX')) ②
def test_to_roman_known_values(self): ③
'''to_roman should give known result with known input'''
for integer, numeral in self.known_values:
result = roman1.to_roman(integer) ④
self.assertEqual(numeral, result) ⑤
if __name__ == '__main__':
unittest.main()
unittest 模塊的TestCase 類的子類。TestCase 提供了很多你可以用于測試特定條件的測試用例的有用的方法。to_roman() 方法. (當然,該方法還沒編寫;但一旦該方法被實現,這就是調用它的行號)。注意,現在你已經為 to_roman() 方法定義了 接口:它必須包含一個整型(被轉換的數字)及返回一個字符串(羅馬數字的表示形式)。如果 接口 實現與這些定義不一致,那么測試就會被視為失敗。同樣,當你調用 to_roman() 時,不要捕獲任何異常。這些都是unittest 故意設計的。當你以有效的輸入調用 to_roman() 時它不會拋出異常。如果 to_roman() 拋出了異常,則測試被視為失敗。to_roman() 方法已經被正確定義,正確調用,成功實現以及返回了一個值,那么最后一步就是去檢查它的返回值是否 right 。這是測試中一個普遍的問題。TestCase 類提供了一個方法 assertEqual 來檢查兩個值是否相等。如果 to_roman() (result) 的返回值跟已知的期望值g (numeral)不一致,則拋出異常,并且測試失敗。如果兩值相等, assertEqual 不會做任何事情。如果 to_roman() 的所有返回值均與已知的期望值一致,則 assertEqual 不會拋出任何異常,于是,test_to_roman_known_values 最終會會正常退出,這就意味著 to_roman() 通過此次測試。一旦你有了測試用例,你就可以開始編寫 to_roman() 方法。首先,你應該用一個空方法作為存根,同時確認該測試失敗。因為如果在編寫任何代碼之前測試已經通過,那么你的測試對你的代碼是完全不會有效果的!單元測試就像跳舞:測試先行,編碼跟隨。編寫一個失敗的測試,然后進行編碼直到該測試通過。
# roman1.py
def to_roman(n):
'''convert integer to Roman numeral'''
pass ①
pass,它恰恰什么都沒做。在命令行上運行 romantest1.py 來執行該測試。如果使用-v命令行參數的話,會有更詳細的輸出來幫助你精確地查看每一條用例的執行過程。幸運的話,你的輸出應該如下:
you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v test_to_roman_known_values (__main__.KnownValues) ① to_roman should give known result with known input ... FAIL ② ====================================================================== FAIL: to_roman should give known result with known input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest1.py", line 73, in test_to_roman_known_values self.assertEqual(numeral, result) AssertionError: 'I' != None ③ ---------------------------------------------------------------------- Ran 1 test in 0.016s ④ FAILED (failures=1) ⑤
unittest.main() , 該方法執行了每一條測試用例。而每一條測試用例都是 romantest.py 中的類方法。這些測試類沒有必要的組織要求;它們每一個都包括一個獨立的測試方法,或者你也可以編寫一個含有多個測試方法的類。唯一的要求就是每一個測試類都必須繼承 unittest.TestCase。unittest 模塊會打印出測試方法的 docstring ,并且說明該測試失敗還是成功。正如預期那樣,該測試用例失敗了。unittest 模塊會打印出詳細的跟蹤信息。在該用例中, assertEqual() 的調用拋出了一個 AssertionError 的異常,這是因為 to_roman(1) 本應該返回 'I' 的,但是它沒有。(因為沒有顯示的返回值,故方法返回了 Python 的空值 None)unittest 打印出一個簡述來說明“多少用例被執行了”和“測試執行了多長時間”。unittest 可以區別用例執行失敗跟程序錯誤的。像 assertXYZ 、assertRaises 這樣的 assertEqual 方法的失敗是因為被聲明的條件不是為真,或者預期的異常沒有拋出。錯誤,則是另一種異常,它是因為被測試的代碼或者單元測試用例本身的代碼問題而引起的。至此,你可以實現 to_roman() 方法了。
roman_numeral_map = (('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 to_roman(n):
'''convert integer to Roman numeral'''
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer: ②
result += numeral
n -= integer
return result
M 到 I)、每一個羅馬數字的阿拉伯數值。每一個內部的元組都是一個(數,值)對。它不但定義了單字符羅馬數字,也定義了雙字符羅馬數字,如CM(“比一千小一百”)。該元組使得 to_roman() 方法實現起來更簡單。如果你仍然不清楚 to_roman() 如何工作,可以在 while 循環末段添加 print() 調用:
while n >= integer:
result += numeral
n -= integer
print('subtracting {0} from input, adding {1} to output'.format(integer, numeral))
因為用于調試的 print() 聲明,輸出會如下:
>>> import roman1 >>> roman1.to_roman(1424) subtracting 1000 from input, adding M to output subtracting 400 from input, adding CD to output subtracting 10 from input, adding X to output subtracting 10 from input, adding X to output subtracting 4 from input, adding IV to output 'MCDXXIV'
這樣, to_roman() 至少在手工檢查下是工作正常的。但它會通過你編寫的測試用例么?
you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok ①
----------------------------------------------------------------------
Ran 1 test in 0.016s
OK
to_roman() 函數通過了“known values” 測試用例。該測試用例并不復雜,但是它的確使該方法按著輸入值的變化而執行,其中的輸入值包括:每一個單字符羅馬數字、最大值數字(3999)、最長字符串數字(3888)。通過這些,你就可以有理由對“該方法接收任何正常的輸入值都工作正常”充滿信心了。“正常”輸入?”嗯。那“非法”輸入呢?
?
僅僅在“正常”值時證明方法通過的測試是不夠的;你同樣需要測試當輸入“非法”值時方法失敗。但并不是說要枚舉所有的失敗類型,而是說必要在你預期的范圍內失敗。
>>> import roman1
>>> roman1.to_roman(4000)
'MMMM'
>>> roman1.to_roman(5000)
'MMMMM'
>>> roman1.to_roman(9000) ①
'MMMMMMMMM'
那問題是:我該如何表達這些內容為可測試需求呢?下面就是一個開始:
當輸入值大于
3999時,to_roman()函數應該拋出一個OutOfRangeError異常。
具體測試代碼如下:
class ToRomanBadInput(unittest.TestCase): ①
def test_too_large(self): ②
'''to_roman should fail with large input'''
self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) ③
unittest.TestCase 的類。你可以在每個類中實現多個測試(正如你在本節中將會看到的一樣),但是我卻選擇了創建一個新類,因為該測試與上一個有點不同。這樣,我們可以把正常輸入的測試跟非法輸入的測試分別放入不同的兩個類中。test 開頭命名。unittest.TestCase 類提供e assertRaises 方法,該方法需要以下參數:你期望的異常、你要測試的方法及傳入給方法的參數。(如果被測試的方法需要多個參數的話,則把所有參數依次傳入 assertRaises, assertRaises 會正確地把參數傳遞給被測方法的。)請關注代碼的最后一行。這里并不需要直接調用 to_roman() ,同時也不需要手動檢查它拋出的異常類型(通過 一個 try...except 塊來包裝),而這些 assertRaises 方法都給我們完成了。你要做的所有事情就是告訴assertRaises你期望的異常類型( roman2.OutOfRangeError)、被測方法(to_roman())以及方法的參數(4000)。assertRaises 方法負責調用 to_roman() 和檢查方法拋出 roman2.OutOfRangeError 的異常。
另外,注意你是把 to_roman() 方法作為參數傳遞;你沒有調用被測方法,也不是把被測方法作為一個字符串名字傳遞進去。我是否在之前提到過 Python 中萬物皆對象有多么輕便?
那么,當你執行該含有新測試的測試套件時,結果如下:
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ERROR ① ====================================================================== ERROR: to_roman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest2.py", line 78, in test_too_large self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) AttributeError: 'module' object has no attribute 'OutOfRangeError' ② ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (errors=1)
OutOfRangeError 的異常。回憶一下,該異常是你傳遞給 assertRaises() 方法的,因為你期望當傳遞給被測試方法一個超大值時可以拋出該異常。但是,該異常并不存在,因此 assertRaises() 的調用會失敗。事實上測試代碼并沒有機會測試 to_roman() 方法,因為它還沒有到達那一步。為了解決該問題,你需要在 roman2.py 中定義 OutOfRangeError 。
class OutOfRangeError(ValueError): ①
pass ②
ValueError 異常類。這并不是嚴格的要求(它同樣也可以繼承于基類 Exception),只要它正確就行了。pass 的真正意思是什么都不做,但是它是一行Python代碼,所以可以使其成為類。再次執行該測試套件。
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... FAIL ① ====================================================================== FAIL: to_roman should fail with large input ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest2.py", line 78, in test_too_large self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) AssertionError: OutOfRangeError not raised by to_roman ② ---------------------------------------------------------------------- Ran 2 tests in 0.016s FAILED (failures=1)
assertRaises() 方法的調用是成功的,同時,單元測試框架事實上也測試了 to_roman() 函數。to_roman() 方法沒有引發你所定義的 OutOfRangeError 異常,因為你并沒有讓它這么做。這真是個好消息!因為它意味著這是個合格的測試案例——在編寫代碼使之通過之前它將會以失敗為結果。現在可以編寫代碼使其通過了。
def to_roman(n):
'''convert integer to Roman numeral'''
if n > 3999:
raise OutOfRangeError('number out of range (must be less than 4000)') ①
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
3999,引發一個 OutOfRangeError 例外。本單元測試并不檢測那些與例外相伴的人類可讀的字符串,但你可以編寫另一個測試來檢查它(但請注意用戶的語言或環境導致的不同國際化問題)。這樣能讓測試通過嗎?讓我們來尋找答案。
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok ①
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
?
與測試超大值一樣,也必須測試超小值。正如我們在功能需求中提到的那樣,羅馬數字無法表達 0 或負數。
>>> import roman2 >>> roman2.to_roman(0) '' >>> roman2.to_roman(-1) ''
顯然,這不是好的結果。讓我們為這些條件逐條添加測試。
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman should fail with large input'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000) ①
def test_zero(self):
'''to_roman should fail with 0 input'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) ②
def test_negative(self):
'''to_roman should fail with negative input'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) ③
test_too_large() 方法跟之前的步驟一樣。我把它包含進來是為了說明新代碼的位置。test_zero() 。如 test_too_large() 一樣,它調用了在n unittest.TestCase 中定義的 assertRaises() 方法,并且以參數值 0 傳入給 to_roman(),最后檢查它拋出相應的異常:OutOfRangeError。test_negative() 也幾乎類似,除了它給 to_roman() 函數傳入 -1 。如果新的測試中 沒有 任何一個拋出了異常 OutOfRangeError (或者由于該函數返回了實際的值,或者由于它拋出了其他類型的異常),那么測試就被視為失敗。檢查測試是否失敗:
you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_negative (__main__.ToRomanBadInput)
to_roman should fail with negative input ... FAIL
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok
test_zero (__main__.ToRomanBadInput)
to_roman should fail with 0 input ... FAIL
======================================================================
FAIL: to_roman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest3.py", line 86, in test_negative
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)
AssertionError: OutOfRangeError not raised by to_roman
======================================================================
FAIL: to_roman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest3.py", line 82, in test_zero
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)
AssertionError: OutOfRangeError not raised by to_roman
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=2)
太棒了!兩個測試都如期地失敗了。接著轉入被測試的代碼并且思考如何才能使得測試通過。
def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 4000): ①
raise OutOfRangeError('number out of range (must be 1..3999)') ②
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
if not ((0 < n) and (n < 4000)),但前者更適合閱讀。這一行代碼應該捕獲那些超大的、負值的或者為 0 的輸入。unittest 框架并不關心這些,但是如果你的代碼拋出描述不正確的異常信息的話會使得手工調試代碼變得困難。我本應該給你展示完整的一系列與本章節不相關的例子來說明一次性多比較的快捷方式是有效的,但是我將僅僅運行本測試用例來證明它的有效性。
you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.016s OK
?
還有一個把阿拉伯數字轉換成羅馬數字的 功能性需求 :處理非整型數字。
>>> import roman3 >>> roman3.to_roman(0.5) ① '' >>> roman3.to_roman(1.0) ② 'I'
測試非整數并不困難。首先,定義一個 NotIntegerError 例外。
# roman4.py
class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
然后,編寫一個檢查 NotIntegerError 例外的案例。
class ToRomanBadInput(unittest.TestCase):
.
.
.
def test_non_integer(self):
'''to_roman should fail with non-integer input'''
self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
然后,檢查該測試是否可以正確地失敗。
you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_negative (__main__.ToRomanBadInput)
to_roman should fail with negative input ... ok
test_non_integer (__main__.ToRomanBadInput)
to_roman should fail with non-integer input ... FAIL
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok
test_zero (__main__.ToRomanBadInput)
to_roman should fail with 0 input ... ok
======================================================================
FAIL: to_roman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest4.py", line 90, in test_non_integer
self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
AssertionError: NotIntegerError not raised by to_roman
----------------------------------------------------------------------
Ran 5 tests in 0.000s
FAILED (failures=1)
編修代碼,使得該測試可以通過。
def to_roman(n):
'''convert integer to Roman numeral'''
if not (0 < n < 4000):
raise OutOfRangeError('number out of range (must be 1..3999)')
if not isinstance(n, int): ①
raise NotIntegerError('non-integers can not be converted') ②
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
isinstance() 方法可以檢查一個變量是否屬于某一類型(或者,技術上的任何派生類型)。int,則拋出新定義的 NotIntegerError 異常。
最后,驗證修改后的代碼的確通過測試。
you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v test_to_roman_known_values (__main__.KnownValues) to_roman should give known result with known input ... ok test_negative (__main__.ToRomanBadInput) to_roman should fail with negative input ... ok test_non_integer (__main__.ToRomanBadInput) to_roman should fail with non-integer input ... ok test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok test_zero (__main__.ToRomanBadInput) to_roman should fail with 0 input ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
to_roman() 方法通過了所有的測試,而且我也想不出別的測試了,因此,下面著手 from_roman()吧!
?
轉換羅馬數字為阿拉伯數字的實現難度聽起來比反向轉換要困難。當然,這種想法不無道理。例如,檢查數值是否比0大容易,而檢查一個字符串是否為有效的羅馬數字則要困難些。但是,我們已經構造了一個用于檢查羅馬數字的規則表,因此規則表的工作可以免了。
現在剩余的工作就是轉換字符串了。正如我們將要看到的一樣,多虧我們定義的用于單個羅馬數字映射至阿拉伯數字的良好的數據結構,from_roman() 的實現本質上與 to_roman() 一樣簡單。
不過,測試先行!為了證明其準確性,我們將需要一個對“已知取值”進行的測試。我們的測試套件已經包含了一個已知取值的映射表,那么,我們就重用它。
def test_from_roman_known_values(self):
'''from_roman should give known result with known input'''
for integer, numeral in self.known_values:
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)
這里看到了令人高興的對稱性。to_roman() 與 from_roman() 函數是互逆的。前者把整型數字轉換為特殊格式化的字符串,而后者則把特殊格式化的字符串轉換為整型數字。理論上,我們應該可以使一個數字“繞一圈”,即把數字傳遞給 to_roman() 方法,得到一個字符串;然后把該字符串傳入 from_roman() 方法,得到一個整型數字,并且跟傳給to_roman()方法的數字是一樣的。
n = from_roman(to_roman(n)) for all values of n
在本用例中,“全有取值”是說 從1到3999 的所有數值,因為這是 to_roman() 方法的有效輸入范圍。為了表達這兩個方法之間的對稱性,我們可以設計這樣的測試用例,它的測試數據集是從1到3999之間(包括1和3999)的所有數值,首先調用 to_roman() ,然后調用 from_roman(),最后檢查輸出是否與原始輸入一致。
class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n for all n'''
for integer in range(1, 4000):
numeral = roman5.to_roman(integer)
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)
這些測試連失敗的機會都沒有。因為我們根本還沒定義 from_roman() 函數,所以它們僅僅會拋出錯誤的結果。
you@localhost:~/diveintopython3/examples$ python3 romantest5.py
E.E....
======================================================================
ERROR: test_from_roman_known_values (__main__.KnownValues)
from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 78, in test_from_roman_known_values
result = roman5.from_roman(numeral)
AttributeError: 'module' object has no attribute 'from_roman'
======================================================================
ERROR: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 103, in test_roundtrip
result = roman5.from_roman(numeral)
AttributeError: 'module' object has no attribute 'from_roman'
----------------------------------------------------------------------
Ran 7 tests in 0.019s
FAILED (errors=2)
一個簡易的留空函數可以解決此問題。
# roman5.py
def from_roman(s):
'''convert Roman numeral to integer'''
(嘿,你注意到了么?我定義了一個除了 docstring 之外沒有任何東西的方法。這是合法的 Python 代碼。事實上,一些程序員喜歡這樣做。“不要留空;寫點文檔!”)
現在測試用力將會失敗。
you@localhost:~/diveintopython3/examples$ python3 romantest5.py
F.F....
======================================================================
FAIL: test_from_roman_known_values (__main__.KnownValues)
from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 79, in test_from_roman_known_values
self.assertEqual(integer, result)
AssertionError: 1 != None
======================================================================
FAIL: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest5.py", line 104, in test_roundtrip
self.assertEqual(integer, result)
AssertionError: 1 != None
----------------------------------------------------------------------
Ran 7 tests in 0.002s
FAILED (failures=2)
現在是時候編寫 from_roman() 函數了。
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral: ①
result += integer
index += len(numeral)
return result
to_roman() 完全相同。遍歷整個羅馬數字數據結構 (一個元組的元組),與前面不同的是不去一個個地搜索最大的整數,而是搜尋 “最大的”羅馬數字字符串。如果不清楚 from_roman() 如何工作,在 while 結尾處添加一個 print 語句:
def from_roman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
print('found', numeral, 'of length', len(numeral), ', adding', integer)
>>> import roman5
>>> roman5.from_roman('MCMLXXII')
found M of length 1, adding 1000
found CM of length 2, adding 900
found L of length 1, adding 50
found X of length 1, adding 10
found X of length 1, adding 10
found I of length 1, adding 1
found I of length 1, adding 1
1972
重新執行一遍測試。
you@localhost:~/diveintopython3/examples$ python3 romantest5.py ....... ---------------------------------------------------------------------- Ran 7 tests in 0.060s OK
這兒有兩個令人激動的消息。一個是 from_roman() 對于所有有效輸入運轉正常,至少對于你測試的已知值是這樣。第二個好消息是,完備性測試也通過了。與已知值測試的通過一起來看,你有理由相信 to_roman() 和 from_roman() 對于所有有效輸入值工作正常。(尚不能完全相信,理論上存在這種可能性: to_roman() 存在錯誤而導致一些特定輸入會產生錯誤的羅馬數字表示,and from_roman() 也存在相應的錯誤,把 to_roman() 錯誤產生的這些羅馬數字錯誤地轉換為最初的整數。取決于你的應用程序和你的要求,你或許需要考慮這個可能性;如果是這樣,編寫更全面的測試用例直到解決這個問題。)
?
現在 from_roman() 對于有效輸入能夠正常工作了,是揭開最后一個謎底的時候了:使它正常工作于無效輸入的情況下。這意味著要找出一個方法檢查一個字符串是不是有效的羅馬數字。這比中驗證有效的數字輸入困難,但是你可以使用一個強大的工具:正則表達式。(如果你不熟悉正則表達式,現在是該好好讀讀正則表達式那一章節的時候了。)
如你在 個案研究:羅馬字母s中所見到的,構建羅馬數字有幾個簡單的規則:使用的字母M , D , C , L , X , V和I 。讓我們回顧一下:
I 是 1, II 是 2,而III 是 3.
VI 是 6 (從字面上理解, “5 和 1”), VII 是 7, 而 VIII 是 8。
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”)。9,你需要從下一個最高十位字符串中減去一個值:8 是 VIII,但 9 是 IX(“ 比 10 小 1”),而不是VIIII (由于 I 字符不能重復四次)。90 是 XC, 900 是 CM。
10 總是表示為 X,而決不能是 VV。
100 總是 C,決不能是 LL。
DC 是 600; CD 則是完全不同的數字 (400, “比 500 小 100 ”)。
CI 是 101; IC 甚至不是合法的羅馬數字(因為你不能直接從 100 減 1;你將不得不將它表示為 XCIX,“比 100 小10 ,然后比 10” 小 1)。因此,有用的測試將會確保 from_roman() 函數應當在傳入太多重復數字時失敗。“太多”是多少取決于數字。
class FromRomanBadInput(unittest.TestCase):
def test_too_many_repeated_numerals(self):
'''from_roman should fail with too many repeated numerals'''
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
另一有效測試是檢查某些未被重復的模式。例如,IX 代表 9,但 IXIX 絕不會合法。
def test_repeated_pairs(self):
'''from_roman should fail with repeated pairs of numerals'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
第三個測試應當檢測數字是否以正確順序出現,從最高到最低位。例如,CL 是 150,而 LC 永遠是非法的,因為代表 50 的數字永遠不能在 100 數字之前出現。
該測試包括一個隨機的可選項:I 在 M 之前, V 在 X 之前,等等。
def test_malformed_antecedents(self):
'''from_roman should fail with malformed antecedents'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
這些測試中的每個都依賴于 from_roman() 引發一個新的例外 InvalidRomanNumeralError,而該例外尚未定義。
# roman6.py
class InvalidRomanNumeralError(ValueError): pass
所有的測試都應該是失敗的,因為 from_roman() 方法還沒有任何有效性檢查。
(如果沒有失敗,它們在測什么呢?)
you@localhost:~/diveintopython3/examples$ python3 romantest6.py
FFF.......
======================================================================
FAIL: test_malformed_antecedents (__main__.FromRomanBadInput)
from_roman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest6.py", line 113, in test_malformed_antecedents
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
AssertionError: InvalidRomanNumeralError not raised by from_roman
======================================================================
FAIL: test_repeated_pairs (__main__.FromRomanBadInput)
from_roman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest6.py", line 107, in test_repeated_pairs
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
AssertionError: InvalidRomanNumeralError not raised by from_roman
======================================================================
FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput)
from_roman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest6.py", line 102, in test_too_many_repeated_numerals
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
AssertionError: InvalidRomanNumeralError not raised by from_roman
----------------------------------------------------------------------
Ran 10 tests in 0.058s
FAILED (failures=3)
好!現在,我們要做的所有事情就是添加正則表達式到 from_roman() 中以測試有效的羅馬數字。
roman_numeral_pattern = re.compile('''
^ # beginning of string
M{0,3} # thousands - 0 to 3 Ms
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 Cs),
# or 500-800 (D, followed by 0 to 3 Cs)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 Xs),
# or 50-80 (L, followed by 0 to 3 Xs)
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 Is),
# or 5-8 (V, followed by 0 to 3 Is)
$ # end of string
''', re.VERBOSE)
def from_roman(s):
'''convert Roman numeral to integer'''
if not roman_numeral_pattern.search(s):
raise InvalidRomanNumeralError('Invalid Roman numeral: {0}'.format(s))
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index : index + len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
再運行一遍測試……
you@localhost:~/diveintopython3/examples$ python3 romantest7.py .......... ---------------------------------------------------------------------- Ran 10 tests in 0.066s OK
本年度的虎頭蛇尾獎頒發給……單詞“OK”,在所有測試通過時,它由 unittest 模塊輸出。
? 2001–9 Mark Pilgrim