7.14. Postscritto

Un lettore intelligente dopo aver letto la sezione precedente ha subito fatto il passo successivo. Il più grosso grattacapo (e peggioratore delle prestazioni) nel programma com'è scritto al momento è l'espressione regolare, che è necessaria perché non abbiamo altro modo di decomporre un numero romano. Ma ci sono solo 5000 numeri romani: perché non ci costruiamo all'inizio una tavola di riferimento e non usiamo quella? Questa idea è ancora migliore una volta capito che è possibile rimuovere del tutto l'uso delle espressioni regolari. Una volta costruita la tavola di riferimento per convertire interi in numeri romani, è possibile costruire la tavola inversa per convertire i numeri romani in interi.

Ma la cosa migliore è che questo lettore aveva già un set completo di test delle unità di codice. Anche se ha cambiato metà del codice nel modulo roman.py, i test delle unità di codice rimanevano gli stessi, cosicché ha potuto verificare che il nuovo codice funzionasse altrettando bene dell'originale.

Esempio 7.38. roman9.py

Se non lo avete ancora fatto, potete scaricare questo ed altri esempi usati in questo libro.

#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, "decimals 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()

E allora, quanto è più veloce?

Esempio 7.39. Output di romantest9.py a fronte di roman9.py


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

OK

Ricordate, le migliori prestazioni mai ottenute nella versione originale erano 13 test in 3.315 secondi. Ovviamente, questo non è un paragone onesto, perché questa versione richiederà più tempo per essere importata (dato che è allora che vengono riempite le tavole di riferimento). Ma dato che il modulo è importato una sola volta, questo tempo è trascurabile in confronto a tutta l'esecuzione del programma.

La morale della favola?