14.3. roman.py,阶段 3

既然 toRoman 能够正确处理有效输入(13999 之间的整数),现在该让它能够正确处理无效输入(其他所有情况)了。

例 14.6. roman3.py

此文件位于示例目录的 py/roman/stage3/ 中。

如果您尚未下载,可以 下载本书中使用的此示例和其他示例

"""Convert to and from Roman numerals"""

#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):                                             1
        raise OutOfRangeError, "number out of range (must be 1..3999)" 2
    if int(n) <> n:                                                    3
        raise NotIntegerError, "non-integers can not be converted"

    result = ""                                                        4
    for numeral, integer in romanNumeralMap:
        while n >= integer:
            result += numeral
            n -= integer
    return result

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
1 这是一个很好的 Python 风格的快捷方式:一次进行多个比较。这等效于 if not ((0 < n) and (n < 4000)),但可读性要好得多。这是范围检查,它应该捕获过大、负数或零的输入。
2 您可以使用 raise 语句自己引发异常。您可以引发任何内置异常,也可以引发您定义的任何自定义异常。第二个参数(错误消息)是可选的;如果给出,则会在打印异常未处理时的回溯信息中显示。
3 这是非整数检查。非整数不能转换为罗马数字。
4 函数的其余部分保持不变。

例 14.7. 观察 toRoman 如何处理无效输入

>>> import roman3
>>> roman3.toRoman(4000)
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "roman3.py", line 27, in toRoman
    raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
>>> roman3.toRoman(1.5)
Traceback (most recent call last):
  File "<interactive input>", line 1, in ?
  File "roman3.py", line 29, in toRoman
    raise NotIntegerError, "non-integers can not be converted"
NotIntegerError: non-integers can not be converted

例 14.8. romantest3.py 针对 roman3.py 的输出

fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... ok 1
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... ok        2
toRoman should fail with negative input ... ok           3
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
1 toRoman 仍然通过了 已知值测试,这令人欣慰。所有在 阶段 2 中通过的测试仍然通过,因此最新的代码没有破坏任何内容。
2 更令人兴奋的是,所有 无效输入测试 现在都通过了。此测试 testNonInteger 之所以通过,是因为 int(n) <> n 检查。当非整数传递给 toRoman 时,int(n) <> n 检查会注意到它并引发 NotIntegerError 异常,这正是 testNonInteger 所期望的。
3 此测试 testNegative 之所以通过,是因为 not (0 < n < 4000) 检查,它会引发 OutOfRangeError 异常,这正是 testNegative 所期望的。

======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 156, in testFromRomanCase
    roman3.fromRoman, numeral.lower())
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 133, in testMalformedAntecedent
    self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 127, in testRepeatedPairs
    self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 122, in testTooManyRepeatedNumerals
    self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 99, in testFromRomanKnownValues
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage3\romantest3.py", line 141, in testSanity
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
----------------------------------------------------------------------
Ran 12 tests in 0.401s

FAILED (failures=6) 1
1 您还有 6 个失败,所有这些都与 fromRoman 有关:已知值测试、三个单独的无效输入测试、大小写检查和健全性检查。这意味着 toRoman 已经通过了它自己可以 通过的所有测试。(它参与了健全性检查,但这还需要编写 fromRoman,而它还没有编写。)这意味着您现在必须停止编写 toRoman 的代码。不要调整,不要摆弄,不要进行额外的“以防万一”检查。停下来。现在。离开键盘。
Note
全面的单元测试可以告诉您的最重要的事情是什么时候停止编码。当函数的所有单元测试都通过时,停止编写该函数的代码。当整个模块的所有单元测试都通过时,停止编写该模块的代码。