7.8. roman.py, fase 3

Adesso che la funzione toRoman si comporta correttamente con input validi (numeri da 1 a 3999), è tempo di fare in modo che lo faccia anche con input non validi (qualsiasi altra cosa).

Esempio 7.12. roman3.py

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

"""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, "decimals 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 Questa è una simpatica scorciatoia in stile Python: confronti multipli in un solo colpo. L'espressione è equivalente a if not ((0 < n) and (n < 4000)), ma è molto più facile da leggere. Questo è il nostro controllo sull'intervallo di ammissibilità e dovrebbe filtrare gli input che sono troppo grandi, negativi o uguali a zero.
2 Con l'istruzione raise si possono sollevare le proprie eccezioni. Si può sollevare una qualsiasi delle eccezioni predefinite, oppure le proprie eccezioni specifiche definite in precedenza. Il secondo parametro, il messaggio di errore, è opzionale; se viene specificato, è visualizzato nel traceback, che viene stampato se l'eccezione non è gestita dal codice.
3 Questo è il controllo sui numeri decimali. I decimali non possono essere convertiti in numeri romani.
4 Il resto della funzione non è cambiata.

Esempio 7.13. Osservare toRoman gestire input non corretti

>>> 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, "decimals can not be converted"
NotIntegerError: decimals can not be converted

Esempio 7.14. Output di romantest3.py a fronte di 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 passa ancora il test sui valori noti, il che è confortante. Tutti i test che erano stati superati nella fase 2 sono superati anche ora, indicando che le ultime modifiche non hanno scombinato niente.
2 Un fatto più eccitante è che tutti i nostri test di input non validi sono stati superati. Questo test, testDecimal, è passato con successo per via del controllo int(n) <> n. Quando un numero decimale è pasato a toRoman, il controllo int(n) <> n lo individua e solleva l'eccezione NotIntegerError, che è quello che si aspetta testDecimal.
3 Questo test, testNegative, è superato con successo per via del controllo not (0 < n < 4000), che solleva una eccezione OutOfRangeError, che è ciò che il test testNegative si aspetta.

======================================================================
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 Siamo sotto di 6 test falliti e tutti riguardano fromRoman: il test sui valori noti, i tre differenti test sugli input non validi, il test sulle maiuscole/minuscole ed il test di consistenza. Questo significa che toRoman ha passato tutti i test che poteva superare da sola (è coinvolta nel test di consistenza, ma questo test richiede anche che fromRoman sia scritta, e questo non è stato ancora fatto.) Questo significa che dobbiamo smettere di codificare toRoman ora. Niente aggiustamenti, niente ritocchi, niente controlli addizionali “giusto per sicurezza”. Basta. Togliete le mani dalla tastiera.
Nota
La cosa più importante che test delle unità di codice, condotti in modo esaustivo, possano comunicarci è il momento in cui si deve smettere di scrivere un programma. Quando tutti i test di un modulo hanno successo, è il momento di smettere di scrivere quel modulo.