Zanurkuj w Pythonie/roman.py, etap 3
Wygląd
(Przekierowano z Python/roman.py, etap 3)
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
- 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.
- 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).
- 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.
- 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
- 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ł.
- 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.
- 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)
- 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.
Jedną z najistotniejszych spraw jest to, że rozumowy unit testing mówi tobie, kiedy przestać kodować. Kiedy funkcja przechodzi wszystkie unit testy przeznaczone dla niej, kończymy kodować tę funkcję. Kiedy wszystkie unit test dla całego modułu zostaną zaliczone, przestajemy kodować moduł. |