Zanurkuj w Pythonie/roman.py, etap 2

Z Wikibooks, biblioteki wolnych podręczników.

Strukturę modułu roman mamy już z grubsza określoną, nadszedł więc czas na napisanie kodu i sprawienie, że nasze testy zaczną w końcu przechodzić.

Przykład 14.3. roman2.py

Plik jest dostępny w katalogu in py/roman/stage2/ wewnątrz katalogu examples.

Jeśli jeszcze tego nie zrobiliście, możecie pobrać ten oraz inne przykłady używane w tej książce stąd.

"""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),                  #(1)
                   ('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"""
    result = ""
    for numeral, integer in romanNumeralMap:
        while n >= integer:                       #(2)
            result += numeral
            n -= integer
    return result

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
  1. romanNumeralMap jest krotką krotek, która definiuje trzy elementy:
    1. reprezentację znakową najbardziej podstawowych liczb rzymskich; zauważcie, że nie są to wyłącznie liczby, których reprezentacja składa się z jednego znaku; zdefiniowane są również pary dwuznakowe, takie jak CM ("o sto mniej niż tysiąc"), dzięki którym kod funkcji toRoman będzie znacznie prostszy
    2. porządek liczb rzymskich; są one uporządkowane malejąco względem ich liczbowej wartości od M do I
    3. wartość liczbową odpowiadającą reprezentacji rzymskiej; każda wewnętrzna krotka jest parą (reprezentacja rzymska, wartość liczbowa)
  2. To jest właśnie miejsce, w którym widać, że opłacało się wprowadzić opisaną wyżej bogatą strukturę danych - nie potrzebujemy żadnej specjalnej logiki do obsłużenia reguły odejmowania. Aby przekształcić wartość liczbową do reprezentacji rzymskiej wystarczy przeiterować po romanNumeralMap szukając najwyższej wartości całkowitej mniejszej bądź równej wartości wejściowej. Po jej znalezieniu dopisujemy odpowiadającą jej reprezentację rzymską na koniec napisu wyjściowego, odejmujemy jej wartość od wartości wejściowej, pierzemy, płuczemy, powtarzamy.

Przykład 14.4. Jak działa toRoman

Jeśli sposób działania funkcji toRoman nie jest całkiem jasny, dodajcie na koniec pętli while instrukcję print:

         while n >= integer:
             result += numeral
             n -= integer
             print 'subtracting', integer, 'from input, adding', numeral, 'to output'
>>> import roman2
>>> roman2.toRoman(1424)
subtracting 1000 from input, adding M to output
subtracting 400 from input, adding CD to output
subtracting 10 from input, adding X to output
subtracting 10 from input, adding X to output
subtracting 4 from input, adding IV to output
'MCDXXIV'

Funkcja toRoman wydaje się działać, przynajmniej w przypadku tego szybkiego, ręcznego sprawdzenia. Czy jednak przechodzi ona testy? Cóż, niezupełnie.

Przykład 14.5. Wyjście programu romantest2.py testującego roman2.py

Pamiętajcie o tym, aby uruchomić romantest2.py z opcją -v w linii poleceń, dzięki czemu włączy się tryb "rozwlekły".

fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok                          #(1)
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               #(2)
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL                    #(3)
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL

  1. Ponieważ w romanNumeralMap reprezentacja liczb rzymskich jest wyrażona przy pomocy wielkich liter, funkcja toRoman rzeczywiście zawsze zwraca napisy złożone z wielkich liter. A więc ten test przechodzi.
  2. Tu pojawia się istotna wiadomość: obecna wersja toRoman przechodzi test znanych wartości. Choć test ten nie jest zbyt wyczerpujący, sprawdza on wiele spośród poprawnych danych wejściowych, wliczając w to wartości, które powinny dać w wyniku każdą reprezentację jednoliterową, największą możliwą wartość (3999) czy też wartość, która daje w wyniku najdłuższą reprezentację rzymską (3888). Na tej podstawie możemy być raczej pewni, że funkcja zwróci poprawną reprezentację dla wszystkich poprawnych danych wejściowych.
  3. Niestety, funkcja "nie działa" dla nieprawidłowych danych wejściowych; nie przechodzi żaden test badający działanie funkcji dla niepoprawnych danych. Ma to sens, ponieważ nie umieściliśmy jeszcze w kodzie funkcji żadnego sprawdzenia dotyczącego błędnych danych. Testy, o których tu mówimy, sprawdzają (używając assertRaises), czy w takich sytuacjach zostaje rzucony odpowiedni wyjątek, a my nigdzie go nie rzucamy. Zrobimy to jednak już w następnym etapie.

Poniżej znajduje się dalszy ciąg wyjścia po uruchomieniu testów jednostkowych, prezentujący szczegóły niepowodzeń. Jest ich aż 10.

======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in testFromRomanCase
    roman2.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\stage2\romantest2.py", line 133, in testMalformedAntecedent
    self.assertRaises(roman2.InvalidRomanNumeralError, roman2.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\stage2\romantest2.py", line 127, in testRepeatedPairs
    self.assertRaises(roman2.InvalidRomanNumeralError, roman2.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\stage2\romantest2.py", line 122, in testTooManyRepeatedNumerals
    self.assertRaises(roman2.InvalidRomanNumeralError, roman2.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\stage2\romantest2.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\stage2\romantest2.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
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 116, in testNonInteger
    self.assertRaises(roman2.NotIntegerError, roman2.toRoman, 0.5)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 112, in testNegative
    self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, -1)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 104, in testTooLarge
    self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 4000)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 108, in testZero
    self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
----------------------------------------------------------------------
Ran 12 tests in 0.320s

FAILED (failures=10)