当前位置: 首页 ‣ 深入 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