Zanurkuj w Pythonie/roman.py, etap 3

Z Wikibooks, biblioteki wolnych podręczników.

roman.py, etap 3[edytuj]

Teraz już toRoman odpowiednio sobie radzi z dobrym wejściem (liczbami całkowitymi od 1 do 3999), więc teraz jest czas zając się niepoprawnym wejściem (wszystkim innym).

Przykład 14.6. roman3.py

Plik ten jest dostępny z py/roman/stage3/ w katalogu przykładów.

Jeśli jeszcze tego nie zrobiłeś, możesz pobrać ten i inne przykłady wykorzystane w tej książce.

"""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. Jest to przyjemny pythonowy skrót: wielokrotne porównanie. Jest to odpowiedniek do if not ((0 < n) and (n < 4000)), jednak łatwiejszy do odczytu. Za pomocą tego kontrolujemy zakres wartości i sprawdzamy, czy wprowadzona liczba nie jest za duża, ujemna, czy też równa zero.
  2. Wyrzucamy wyjątek za pomocą wyrażenia raise. Możemy wyrzucić każdy wbudowane wyjątek, a także inny zdefiniowany przez nas wyjątek. Drugi parametr, wiadomość błędu, jest opcjonalny; jeśli dostaniemy wyjątek i nigdzie jego nie obsłużymy, zostanie on wyświetlone w traceback (w postaci śladów stosu).
  3. Za pomocą tego sprawdzamy, czy liczba nie jest całkowita. Liczby nie będące liczbami całkowitymi nie mogą zostać przekonwertowane na system rzymski.
  4. Pozostała część funkcji jest niezmieniona.

Przykład 14.7. Obserwujemy, jak toRoman radzi sobie z błędnym wejściem

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

Przykład 14.8. Wyjście romantest3.py w zależności od 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 dalej przechodzi testy o znanych wartościach, co jest pocieszające. Ponadto przechodzi wszystkie testy, które przechodził w etapie 2, zatem ostatni kod niczego nie popsuł.
  2. Bardziej ekscytujący jest fakty, że teraz nasz program przechodzi wszystkie testy z niepoprawnym wejściem. Przechodzi ten test (czyli testNonInteger), ponieważ kontrolujemy, czy int(n) <> n. Kiedy do funkcji toRoman zostanie przekazana wartość nie będąca liczbą całkowitą, porównanie int(n) <> n wyłapie to i wyrzuci wyjątek NotIntegerError, a tego oczekuje test testNonInteger.
  3. Program przechodzi ten test (test testNegative), ponieważ w przypadku prawdziwości wyrażenia not (0 < n < 4000) zostanie wyrzucony wyjątek OutOfRangeError, a którego oczekuje test 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. Teraz liczba niezaliczonych testów zmniejszyła się do 6 i wszystkie je powoduje fromRoman, czyli: test znanych wartości, trzy testy dotyczące niepoprawnych argumentów, kontrola wielkości znaków i kontrola zdroworozsądkowa (czyli fromRoman(toRoman(n))==n). Oznacza to, że toRoman przeszedł wszystkie testy, które mógł przejść samemu. (Nawala w teście zdroworozsądkowym, ale test ten wymaga także napisania funkcji fromRoman, a to jeszcze nie zostało zrobione.) Oznacza to, że musimy przestać już kodować toRoman. Już nie ulepszamy, nie kombinujemy, bez ekstra "a może ten". Stop. Teraz odejdziemy od klawiatury.