15.4. 后记

一位聪明的读者阅读了上一节并将其提升到了一个新的水平。当前程序中最大的问题(也是性能瓶颈)是正则表达式,这是必需的,因为您没有其他方法来分解罗马数字。但它们只有 5000 个;为什么不构建一个查找表,然后直接读取呢?当您意识到根本不需要使用正则表达式时,这个想法就更好了。在构建用于将整数转换为罗马数字的查找表时,您可以构建反向查找表以将罗马数字转换为整数。

最棒的是,他已经有一套完整的单元测试。他更改了模块中一半以上的代码,但单元测试保持不变,因此他可以证明他的代码与原始代码一样有效。

示例 15.17. roman9.py

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

如果您还没有这样做,您可以下载本书中使用的此示例和其他示例

#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass

#Roman numerals must be less than 5000
MAX_ROMAN_NUMERAL = 4999

#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))

#Create tables for fast conversion of roman numerals.
#See fillLookupTables() below.
toRomanTable = [ None ]  # Skip an index since Roman numerals have no zero
fromRomanTable = {}

def toRoman(n):
    """convert integer to Roman numeral"""
    if not (0 < n <= MAX_ROMAN_NUMERAL):
        raise OutOfRangeError, "number out of range (must be 1..%s)" % MAX_ROMAN_NUMERAL
    if int(n) <> n:
        raise NotIntegerError, "non-integers can not be converted"
    return toRomanTable[n]

def fromRoman(s):
    """convert Roman numeral to integer"""
    if not s:
        raise InvalidRomanNumeralError, "Input can not be blank"
    if not fromRomanTable.has_key(s):
        raise InvalidRomanNumeralError, "Invalid Roman numeral: %s" % s
    return fromRomanTable[s]

def toRomanDynamic(n):
    """convert integer to Roman numeral using dynamic programming"""
    result = ""
    for numeral, integer in romanNumeralMap:
        if n >= integer:
            result = numeral
            n -= integer
            break
    if n > 0:
        result += toRomanTable[n]
    return result

def fillLookupTables():
    """compute all the possible roman numerals"""
    #Save the values in two global tables to convert to and from integers.
    for integer in range(1, MAX_ROMAN_NUMERAL + 1):
        romanNumber = toRomanDynamic(integer)
        toRomanTable.append(romanNumber)
        fromRomanTable[romanNumber] = integer

fillLookupTables()

那么它有多快?

示例 15.18. romantest9.py 针对 roman9.py 的输出


.............
----------------------------------------------------------------------
Ran 13 tests in 0.791s

OK

请记住,您在原始版本中获得的最佳性能是在 3.315 秒内完成 13 次测试。当然,这不是完全公平的比较,因为此版本导入所需的时间更长(当它填充查找表时)。但由于导入只进行一次,因此从长远来看,这可以忽略不计。

这个故事的寓意是什么?