Itt vagy: Kezdőlap ‣ Ugorj fejest a Python 3-ba ‣
Nehézségi szint: ♦♦♢♢♢
❝ A bizonyosság nem egyenlő a bizonysággal. Rengeteg dologban voltunk halálosan biztosak, amelyek másképp voltak. ❞
– Oliver Wendell Holmes, Jr.
Ezek a mai fiatalok. Úgy elkényeztetik őket ezek a gyors számítógépek és az elegáns „dinamikus” nyelvek. Írd meg, add ki, keress hibákat (már ha egyáltalán). Az én időmben fegyelem volt. Mondom fegyelem! A programokat kézzel írtuk papírra, és a számítógépbe lyukkártyákon vittük be. És szerettük!
Ebben a fejezetben római számokat oda-vissza átalakító segédfüggvényeket fogsz írni, és megkeresed a hibáikat. Az „Esettanulmány: római számok” fejezetben láttad a római számok összeállításának és ellenőrzésének működését. Most tegyél egy lépést hátra, és gondold át, mi kellene ennek kétirányú segédprogrammá fejlesztéséhez.
A római számok szabályai számos érdekes megfigyelésre vezetnek:
1
és 3999
közti számok. A rómaiak több módon is le tudtak írni nagyobb számokat, például a szám fölé húzott vonallal jelölték, hogy annak normális értékét meg kell szorozni 1000
-rel. Ezen fejezet szempontjából elég csak az 1
és 3999
közötti római számokkal foglalkozni.
Kezdjük el feltérképezni, mit kell egy roman.py
modulnak csinálnia. Két fő függvénye lesz, a to_roman()
és a from_roman()
. A to_roman()
függvény egy 1
és 3999
közti egészet vár, és visszaadja a római számokkal írt változatát karakterláncként…
Itt álljunk meg. Csináljunk valami enyhén váratlant: írjunk egy tesztesetet, amely ellenőrzi, hogy a to_roman()
függvény azt csinálja-e, amit vársz tőle. Jól olvastad: olyan kódot fogsz írni, amely a még meg sem írt kódodat teszteli.
Ezt tesztvezérelt fejlesztésnek vagy TDD-nek hívják. A két átalakítási függvény – to_roman()
és később a from_roman()
– megírható és tesztelhető egy egységként, függetlenül bármely nagyobb programtól, amely importálja. A Python rendelkezik egy keretrendszerrel az egységteszteléshez, ezt a unittest
modul tartalmazza.
Az egységtesztelés a tesztközpontú fejlesztési stratégia fontos része. Ha egységteszteket írsz, akkor fontos azokat időben megírni, és a követelmények változásával együtt frissíteni. Sokan népszerűsítik a tesztek megírását a tesztelendő kód megírása előtt, és ezt a stílus mutatom be ebben a fejezetben. De az egységtesztek hasznosak, akármikor is írod meg azokat.
⁂
Egy teszteset egyetlen kérdést válaszol meg az általa tesztelt kóddal kapcsolatban. Egy tesztesetnek képesnek kell lennie...
Ezeket figyelembe véve készítsünk egy tesztesetet az első követelményhez:
to_roman()
függvénynek vissza kell adnia az 1
és 3999
közti egészek római számokkal való ábrázolását.
Nem azonnal nyilvánvaló, hogy az alábbi kód hogyan is csinál… nos, bármit. Definiál egy osztályt, amelynek nincs __init__()
metódusa. Az osztály rendelkezik egy másik metódussal, de az soha nem kerül meghívásra. A teljes parancsfájlnak van egy __main__
blokkja, de az nem hivatkozik az osztályra vagy annak a metódusára. De valamit azért csinál, ígérem.
import roman1
import unittest
class KnownValues(unittest.TestCase): ①
known_values = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX')) ②
def test_to_roman_known_values(self): ③
'''a to_roman ismert eredményt kell adjon ismert bemenetre'''
for integer, numeral in self.known_values:
result = roman1.to_roman(integer) ④
self.assertEqual(numeral, result) ⑤
if __name__ == '__main__':
unittest.main()
unittest
modul TestCase
osztályából. Ez az osztály sok hasznos metódust biztosít, amelyeket a tesztesetedben adott helyzetek tesztelésére használhatsz.
test
szóval kell kezdődnie. Ha egy tesztmetódus normálisan lép ki, kivétel dobása nélkül, akkor sikeresnek tekintjük, ha kivételt dob, akkor sikertelennek.
to_roman()
függvényt. (Illetve a függvény még nincs megírva, de ha meg lesz, akkor majd ez a sor fogja meghívni. Figyeld meg, hogy most definiáltad a to_roman()
függvény API-ját: egy egész számot (az átalakítandó számot) vár, és egy karakterláncot ad vissza (a római számokkal való ábrázolást). Ha az API ettől eltér, akkor a teszt sikertelennek lesz tekintve. Figyeld meg azt is, hogy nem fogsz el kivételeket a to_roman()
hívásakor. Ez szándékos. A to_roman()
függvénynek nem kell kivételt dobnia, amikor érvényes bemenettel hívod, és ezek a bemeneti értékek mind érvényesek. Ha a to_roman()
kivételt dob, akkor a teszt sikertelennek lesz tekintve.
to_roman()
függvény helyesen lett definiálva, helyesen lett meghívva, sikeresen befejeződött, és visszaadott egy értéket, az utolsó lépés annak ellenőrzése, hogy a helyes értéket adta-e vissza. Ez egy gyakori kérdés, és a TestCase
osztály biztosítja az assertEqual
metódust, amely ellenőrzi, hogy a két érték egyenlő-e. Ha a to_roman()
által visszaadott eredmény (result) nem egyezik a várt értékkel (numeral), akkor az assertEqual
kivételt dob, és a teszt sikertelen lesz. Ha a két érték egyenlő, akkor az assertEqual
nem csinál semmit. Ha a to_roman()
által visszaadott összes érték egyezik a vár ismert értékkel, akkor az assertEqual
soha nem dob kivételt, így a test_to_roman_known_values
végül normálisan lép ki, azaz a to_roman()
átment ezen a teszten.
Ha már van egy teszteseted, akkor elkezdheted a to_roman()
függvény megírását. Először is egy üres csonkot kell létrehoznod, és meg kell győződnöd róla, hogy a tesztek nem sikerülnek. Ha a tesztek az előtt sikerülnek, hogy bármilyen kódot is írtál volna, akkor a tesztjeid egyáltalán nem tesztelik a kódod! Az egységtesztelés egy tánc: a tesztek vezetnek, a kód követi. Írj egy sikertelen tesztet, majd kódolj amíg nem sikerül.
# roman1.py
def to_roman(n):
'''egész szám római számmá alakítása'''
pass ①
to_roman()
függvény API-ját, de még nem kell megírni. (Először a tesztnek sikertelennek kell lennie. A csonk elkészítéséhez használd a Python pass
foglalt szavát, amely semmit nem csinál.
Futtasd a romantest1.py
fájlt a parancssorból a teszt futtatásához. Ha a -v
parancssori kapcsolóval hívod, akkor részletesebb kimenetet ad, így pontosan láthatod, mi történik az egyes tesztesetek futtatásakor. Kis szerencsével a kimenet valahogy így fog kinézni:
you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v test_to_roman_known_values (__main__.KnownValues) ① a to_roman ismert eredményt kell adjon ismert bemenetre ... FAIL ② ====================================================================== FAIL: a to_roman ismert eredményt kell adjon ismert bemenetre ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest1.py", line 73, in test_to_roman_known_values self.assertEqual(numeral, result) AssertionError: 'I' != None ③ ---------------------------------------------------------------------- Ran 1 test in 0.016s ④ FAILED (failures=1) ⑤
unittest.main()
metódust futtatja, amely lefuttatja az egyes teszteseteket. Minden teszteset egy metódus a romantest.py
egy osztályán belül. A tesztosztályokat nem kell rögzített módon szervezni: mindegyik tartalmazhat egy tesztmetódust vagy többet is. Az egyetlen követelmény, hogy minden egyes tesztosztálynak a unittest.TestCase
osztályból kell származnia.
unittest
modul ki fogja írni a metódus docstring-jét
, és hogy a teszt sikerült-e. Ahogy vártuk, ez a teszteset nem sikerült.
unittest
megjeleníti a nyomkövetési információkat, amelyekből kiderül, hogy mi történt. Ebben az esetben az assertEqual()
hívás AssertionError
kivételt dobott, mert a to_roman(1)
hívásnak az 'I'
értéket kellett volna visszaadnia, de nem ez történt. (Mivel nem volt megadva a return utasítás, a függvény a None
-t, a Python null értékét adta vissza.)
unittest
összegzi, hogy hány tesztet hajtott végre, és ez mennyi ideig tartott.
unittest
megkülönbözteti a sikertelenséget és a hibát. A sikertelenség egy assertXYZ
metódus hívását jelenti, mint az assertEqual
vagy assertRaises
, amely azért sikertelen, mert a kijelentésben szereplő feltétel nem igaz, vagy a metódus nem dobott egy várt kivételt. A hiba egy tetszőleges típusú egyéb kivétel, amely a tesztelt kódban vagy magában az egységtesztesetben keletkezett.
Most, végre megírhatod a to_roman()
függvényt.
roman_numeral_map = (('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 to_roman(n):
'''egész szám római számmá alakítása'''
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer: ②
result += numeral
n -= integer
return result
M
-től I
-ig) és az egyes római számok értékét. Minden belső tuple egy (római szám, érték)
pár. Nem csak az egy karakterből álló római számok, hanem két karakterből álló párokat is definiál, mint a CM
(„százzal kevesebb ezernél”). Ez egyszerűbbé teszi a to_roman()
függvény kódját.
Ha még nem teljesen világos, hogyan működik a to_roman()
függvény, akkor adj egy print()
hívást a while
ciklus végéhez:
while n >= integer:
result += numeral
n -= integer
print('{0} kivonása a bemenetből, {1} hozzáadása a kimenethez'.format(integer, numeral))
A hibakeresési print()
utasításokkal a kimenet így néz ki:
>>> import roman1 >>> roman1.to_roman(1424) 1000 kivonása a bemenetből, M hozzáadása a kimenethez 400 kivonása a bemenetből, CD hozzáadása a kimenethez 10 kivonása a bemenetből, X hozzáadása a kimenethez 10 kivonása a bemenetből, X hozzáadása a kimenethez 4 kivonása a bemenetből, IV hozzáadása a kimenethez 'MCDXXIV'
Így a to_roman()
függvény működni látszik, legalábbis ezen a kézi ellenőrzésen átmegy. De átmegy az általad írt teszteseten is?
you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
test_to_roman_known_values (__main__.KnownValues)
a to_roman ismert eredményt kell adjon ismert bemenetre ... ok ①
----------------------------------------------------------------------
Ran 1 test in 0.016s
OK
to_roman()
függvény átmegy az „ismert értékek” teszteseten. Ez nem átfogó, de ráküldi a függvényt különböző bemenetekre, beleértve az összes egy karakteres római számot, a legnagyobb lehetséges értéket (3999
), és a leghosszabb lehetséges római számot (3888
) előállító bemeneteket. Ezen a ponton meglehetősen biztos lehetsz abban, hogy a függvény bármely jó bemenetre működik, amit csak meg tudsz neki adni.
„Jó” bemenet? Hmm. Mi van a rossz bemenettel?
⁂
Nem elengendő azt tesztelni, hogy a függvények működnek-e jó bemenet megadása esetén; arról is meg kell győződnöd, hogy rossz bemenet esetén megszakítják a működésüket. Ráadásul nem „csak úgy” szakítják meg a működésüket, hanem úgy, ahogyan azt várod.
>>> import roman1
>>> roman1.to_roman(4000)
'MMMM'
>>> roman1.to_roman(5000)
'MMMMM'
>>> roman1.to_roman(9000) ①
'MMMMMMMMM'
A kérdés, amit fel kell tenned magadnak: „Hogyan fejezhetem ki ezt tesztelhető követelményként?” Kezdésnek mit szólnál ehhez:
A
to_roman()
függvénynekOutOfRangeError
kivételt kell dobnia, ha a kapott egész szám nagyobb, mint3999
.
Hogy nézne ki ez a teszt?
import unittest, roman2
class ToRomanBadInput(unittest.TestCase): ①
def test_too_large(self): ②
'''a to_roman nem engedélyezhet túl nagy bemenetet'''
self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) ③
unittest.TestCase
-ből származó osztályt kell létrehoznod. Osztályonként több tesztünk is lehet (ahogyan a fejezetben később látni fogod), de itt új osztály létrehozását választottam, mert ez a teszt jelentősen különbözik az előzőtől. A jó bemenet tesztjeit egy osztályban tartjuk, a rossz bemenet tesztjeit pedig egy másikban.
test
szóval kezdődik.
unittest.TestCase
osztály biztosítja az assertRaises
metódust, amely a következő argumentumokat várja: a várt kivétel, a tesztelt függvény, a függvénynek átadott argumentumok. (Ha a tesztelt függvény több argumentumot vár, akkor add át sorrendben az összeset az assertRaises
metódusnak, és az át fogja azokat adni a tesztelt függvénnyel együtt.)
Figyeld meg jól ezt az utolsó kódsort. A to_roman()
közvetlen hívása és a bizonyos kivétel dobásának kézi ellenőrzése (egy try...except
blokkba ágyazással) helyett az assertRaises
metódus ezt mind elvégezte nekünk. Mindössze a várt kivételt (roman2.OutOfRangeError
), a függvényt (to_roman()
) és a függvény argumentumait (4000
) kell megnevezni. Az assertRaises
metódus elvégzi a to_roman()
hívását, és annak ellenőrzését, hogy az dobott-eroman2.OutOfRangeError
kivételt.
Vedd észre azt is, hogy a to_roman()
függvényt magát argumentumként adod át, nem pedig meghívod, vagy karakterláncként adod át a nevét. Említettem korábban, hogy mennyire kézreálló, hogy Pythonban minden objektum?
Mi történik tehát, ha a tesztcsomagot ezzel az új teszttel együtt futtatod?
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v test_to_roman_known_values (__main__.KnownValues) a to_roman ismert eredményt kell adjon ismert bemenetre ... ok test_too_large (__main__.ToRomanBadInput) a to_roman nem engedélyezhet túl nagy bemenetet ... ERROR ① ====================================================================== ERROR: a to_roman nem engedélyezhet túl nagy bemenetet ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest2.py", line 78, in test_too_large self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) AttributeError: 'module' object has no attribute 'OutOfRangeError' ② ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (errors=1)
OutOfRangeError
nevű kivétellel. Emlékezz, ezt a kivételt átadtad az assertRaises()
metódusnak, mert ez az a kivétel, amelyet a függvénnyel dobatni szeretnél, ha tartományon kívüli bemenetet kap. Azonban a kivétel nem létezik, így az assertRaises()
metódus hívása nem sikerült. Soha nem volt lehetősége a to_roman()
függvény tesztelésére, nem jutott el addig.
A probléma megoldásához definiálnod kell az OutOfRangeError
kivételt a roman2.py
fájlban.
class OutOfRangeError(ValueError): ①
pass ②
ValueError
(értékhiba) kivételből származik. Ez nem kötelező (származhatna épp az alap Exception
osztályból is), de helyesnek érződik.
pass
hívása egész pontosan semmit sem csinál, de ez egy sor Python kód, így megvan az osztály.
Most futtassuk újra a tesztcsomagot.
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v test_to_roman_known_values (__main__.KnownValues) a to_roman ismert eredményt kell adjon ismert bemenetre ... ok test_too_large (__main__.ToRomanBadInput) a to_roman nem engedélyezhet túl nagy bemenetet ... FAIL ① ====================================================================== FAIL: a to_roman nem engedélyezhet túl nagy bemenetet ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest2.py", line 78, in test_too_large self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000) AssertionError: OutOfRangeError not raised by to_roman ② ---------------------------------------------------------------------- Ran 2 tests in 0.016s FAILED (failures=1)
assertRaises()
metódus ez alkalommal sikeres volt, és az egységtesztelő keretrendszer ténylegesen tesztelte a to_roman()
függvényt.
to_roman()
függvény még mindig nem dobja az imént definiált OutOfRangeError
kivételt, mert még nem írtad meg az ehhez szükséges kódot. Kitűnő hír! Ez azt jelenti, hogy ez egy érvényes teszteset – sikertelen, mielőtt megírnád a kódot, amely átmegy rajta.
Itt az ideje megírni a kódot, amely átmegy a teszten.
def to_roman(n):
'''egész szám római számmá alakítása'''
if n > 3999:
raise OutOfRangeError('a szám kívül esik a tartományon (4000-nél kisebb kell legyen)') ①
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
3999
, akkor dobjon OutOfRangeError
kivételt. Az egységteszt nem ellenőrzi a kivételt kísérő, emberek által olvasható karakterláncot, noha írhatnál egy másik tesztet, amely ezt ellenőrzi (de figyelj oda a felhasználó nyelvétől vagy környezetétől függő karakterláncokkal kapcsolatos nemzetköziesítési problémákra).
Ettől vajon átmegy a teszt? Nézzük meg.
you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
test_to_roman_known_values (__main__.KnownValues)
a to_roman ismert eredményt kell adjon ismert bemenetre ... ok
test_too_large (__main__.ToRomanBadInput)
a to_roman nem engedélyezhet túl nagy bemenetet ... ok ①
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
⁂
A túl nagy számok tesztelésével együtt tesztelned kell a túl kicsi számokat is. Amint a funkcionális követelmények között megjegyeztük, a római számokkal nem lehet a 0-t vagy negatív számokat kifejezni.
>>> import roman2 >>> roman2.to_roman(0) '' >>> roman2.to_roman(-1) ''
Hát ez nem jó. Készítsünk teszteket ezen helyzetek mindegyikére.
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''a to_roman nem engedélyezhet túl nagy bemenetet'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000) ①
def test_zero(self):
'''a to_roman nem engedélyezheti a 0 bemenetet'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) ②
def test_negative(self):
'''a to_roman nem engedélyezhet negatív bemenetet'''
self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) ③
test_too_large()
metódus nem változott az előző lépés óta. Csak azért van itt, hogy lásd, hova kerül az új kód.
test_zero()
metódus. A test_too_large()
metódushoz hasonlóan megadja az unittest.TestCase
osztály assertRaises()
metódusának, hogy hívja meg a to_roman()
függvényt a 0 argumentummal, és ellenőrizze, hogy a megfelelő OutOfRangeError
kivételt dobja-e.
test_negative()
metódus majdnem azonos, kivéve hogy a -1
értéket adja át a to_roman()
függvénynek. Ha ezen új tesztek valamelyike nem dob OutOfRangeError
kivételt (vagy mert a függvény egy tényleges értéket ad vissza, vagy mert valami más kivételt dob), akkor a teszt sikertelennek tekintendő.
Most ellenőrizzük, hogy a tesztek nem sikerülnek:
you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v test_to_roman_known_values (__main__.KnownValues) a to_roman ismert eredményt kell adjon ismert bemenetre ... ok test_negative (__main__.ToRomanBadInput) a to_roman nem engedélyezhet negatív bemenetet ... FAIL test_too_large (__main__.ToRomanBadInput) a to_roman nem engedélyezhet túl nagy bemenetet ... ok test_zero (__main__.ToRomanBadInput) a to_roman nem engedélyezheti a 0 bemenetet ... FAIL ====================================================================== FAIL: a to_roman nem engedélyezhet negatív bemenetet ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest3.py", line 86, in test_negative self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1) AssertionError: OutOfRangeError not raised by to_roman ====================================================================== FAIL: a to_roman nem engedélyezheti a 0 bemenetet ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest3.py", line 82, in test_zero self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0) AssertionError: OutOfRangeError not raised by to_roman ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=2)
Kitűnő. Mindkét teszt sikertelen, ahogy vártuk. Most váltsunk át a kódra, és nézzük meg, hogy mit tehetünk azért, hogy sikerüljenek.
def to_roman(n):
'''egész szám római számmá alakítása'''
if not (0 < n < 4000): ①
raise OutOfRangeError('a szám kívül esik a tartományon (1 és 3999 közti kell legyen)') ②
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
if not ((0 < n) and (n < 4000))
, de sokkal olvashatóbb. Ennek a kódsornak meg kell fognia a túl nagy, negatív vagy nulla bemeneteket.
unittest
keretrendszert nem érdekli, de megnehezíti a kézi hibakeresést, ha a kódod helytelenül leírt kivételeket dob.
Egész sor független példát hozhatnék arra, hogy a „több összehasonlítás egyszerre” rövidítés működik, de inkább csak lefuttatom az egységteszteket, és bebizonyítom.
you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v test_to_roman_known_values (__main__.KnownValues) a to_roman ismert eredményt kell adjon ismert bemenetre ... ok test_negative (__main__.ToRomanBadInput) a to_roman nem engedélyezhet negatív bemenetet ... ok test_too_large (__main__.ToRomanBadInput) a to_roman nem engedélyezhet túl nagy bemenetet ... ok test_zero (__main__.ToRomanBadInput) a to_roman nem engedélyezheti a 0 bemenetet ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.016s OK
⁂
Volt még egy működési követelmény a számok római számokká alakításához: a nem egész számok kezelése.
>>> import roman3 >>> roman3.to_roman(0.5) ① '' >>> roman3.to_roman(1.0) ② 'I'
A nem egész számok tesztelése nem nehéz. Először egy NotIntegerError
kivételt kell definiálni.
# roman4.py
class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
Ezután írjunk egy tesztesetet, amely a NotIntegerError
kivételt ellenőrzi.
class ToRomanBadInput(unittest.TestCase):
.
.
.
def test_non_integer(self):
'''a to_roman nem engedélyezhet nem egész bemenetet'''
self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
Most lássuk, hogy a teszt nem sikerül-e, ahogy azt várnánk.
you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v test_to_roman_known_values (__main__.KnownValues) a to_roman ismert eredményt kell adjon ismert bemenetre ... ok test_negative (__main__.ToRomanBadInput) a to_roman nem engedélyezhet negatív bemenetet ... ok test_non_integer (__main__.ToRomanBadInput) a to_roman nem engedélyezhet nem egész bemenetet ... FAIL test_too_large (__main__.ToRomanBadInput) a to_roman nem engedélyezhet túl nagy bemenetet ... ok test_zero (__main__.ToRomanBadInput) a to_roman nem engedélyezheti a 0 bemenetet ... ok ====================================================================== FAIL: a to_roman nem engedélyezhet nem egész bemenetet ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest4.py", line 90, in test_non_integer self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5) AssertionError: NotIntegerError not raised by to_roman ---------------------------------------------------------------------- Ran 5 tests in 0.000s FAILED (failures=1)
Írjuk meg a kódot, amelynek hatására a teszt sikerülni fog.
def to_roman(n):
'''egész szám római számmá alakítása'''
if not (0 < n < 4000):
raise OutOfRangeError('a szám kívül esik a tartományon (1 és 3999 közti kell legyen)')
if not isinstance(n, int): ①
raise NotIntegerError('a nem egész számok nem alakíthatók át') ②
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
isinstance()
függvény teszteli, hogy egy változó adott típusú-e (technikailag: bármely leszármazott típusú-e).
int
, akkor az újonnan készített NotIntegerError
kivételt dobja.
Végül ellenőrizzük, hogy a kód tényleg átmegy-e a teszten.
you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v test_to_roman_known_values (__main__.KnownValues) a to_roman ismert eredményt kell adjon ismert bemenetre ... ok test_negative (__main__.ToRomanBadInput) a to_roman nem engedélyezhet negatív bemenetet ... ok test_non_integer (__main__.ToRomanBadInput) a to_roman nem engedélyezhet nem egész bemenetet ... ok test_too_large (__main__.ToRomanBadInput) a to_roman nem engedélyezhet túl nagy bemenetet ... ok test_zero (__main__.ToRomanBadInput) a to_roman nem engedélyezheti a 0 bemenetet ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
A to_roman()
függvény átmegy az összes tesztjén, és nem tudok kitalálni több tesztet, úgyhogy ideje a from_roman()
függvénnyel folytatni.
⁂
Egy karakterlánc római számból egészszé alakítása nehezebbnek hangzik, mint egy egész átalakítása római számmá. Minden bizonnyal itt van az ellenőrzés problémája. Egyszerű azt ellenőrizni, hogy egy egész nagyobb-e, mint 0, de egy kicsit nehezebb azt ellenőrizni, hogy egy karakterlánc érvényes római szám-e. Azonban már összeállítottunk egy reguláris kifejezést, amely a római számokat ellenőrzi, így ez a rész kész van.
Emiatt már csak a karakterlánc átalakításának problémáját kell megoldani. Ahogy egy perc múlva látni fogjuk, az egyes római számok egész értékekre való leképezéséhez definiált gazdag adatszerkezetnek köszönhetően, a from_roman()
függvény igazán kemény része ugyanolyan magától értetődő, mint a to_roman()
függvényé.
De előbb a tesztek. Szükségünk lesz egy „ismert értékek” tesztre a pontosság azonnali ellenőrzéséhez. A tesztcsomagunk már tartalmazza ismert értékek leképezését; használjuk újra azt.
def test_from_roman_known_values(self):
'''a from_roman ismert eredményt kell adjon ismert bemenetre'''
for integer, numeral in self.known_values:
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)
Van itt egy kellemes szimmetria. A to_roman()
és a from_roman()
függvények egymás inverzei. Az első egészeket alakít át speciálisan formázott karakterláncokká, a második speciálisan formázott karakterláncokat alakít egészekké. Elméletben képesnek kellene lennünk egy szám „körbejárására”: a to_roman()
függvénynek átadva kapott karakterláncot átadva a from_roman()
függvénynek ugyanazt az egész számot kellene visszakapnunk.
n = from_roman(to_roman(n)) az n minden értékére
Ebben az esetben a „minden érték” az 1 és 3999
közti tetszőleges számot jelent, mert ez a to_roman()
függvény érvényes bemeneti tartománya. Ezt a szimmetriát kifejezhetjük egy olyan tesztesettel, amely végigfut az összes értéken 1..3999
között, meghívja a to_roman()
, majd a from_roman()
függvényt, és ellenőrzi, hogy a kimenet ugyanaz-e, mint az eredeti bemenet.
class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n minden n-re'''
for integer in range(1, 4000):
numeral = roman5.to_roman(integer)
result = roman5.from_roman(numeral)
self.assertEqual(integer, result)
Ezek az új tesztek sikertelenek sem lesznek. Még egyáltalán nem definiáltunk from_roman()
nevű függvényt, így eredményül csupán hibákat kapunk.
you@localhost:~/diveintopython3/examples$ python3 romantest5.py E.E.... ====================================================================== ERROR: test_from_roman_known_values (__main__.KnownValues) a from_roman ismert eredményt kell adjon ismert bemenetre ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 78, in test_from_roman_known_values result = roman5.from_roman(numeral) AttributeError: 'module' object has no attribute 'from_roman' ====================================================================== ERROR: test_roundtrip (__main__.RoundtripCheck) from_roman(to_roman(n))==n minden n-re ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 103, in test_roundtrip result = roman5.from_roman(numeral) AttributeError: 'module' object has no attribute 'from_roman' ---------------------------------------------------------------------- Ran 7 tests in 0.019s FAILED (errors=2)
Egy gyors függvénycsonk megoldja ezt a problémát.
# roman5.py
def from_roman(s):
'''római számok egésszé alakítása'''
(Hé, figyeled ezt? Egy olyan függvényt definiáltam, amely nem tartalmaz semmit, csak egy docstringet. Ez érvényes Python. Tulajdonképpen néhány programozó esküszik rá. „Ne csonkolj, dokumentálj!”)
Most a tesztesetek ténylegesen sikertelenek lesznek.
you@localhost:~/diveintopython3/examples$ python3 romantest5.py F.F.... ====================================================================== FAIL: test_from_roman_known_values (__main__.KnownValues) a from_roman ismert eredményt kell adjon ismert bemenetre ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 79, in test_from_roman_known_values self.assertEqual(integer, result) AssertionError: 1 != None ====================================================================== FAIL: test_roundtrip (__main__.RoundtripCheck) from_roman(to_roman(n))==n minden n-re ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest5.py", line 104, in test_roundtrip self.assertEqual(integer, result) AssertionError: 1 != None ---------------------------------------------------------------------- Ran 7 tests in 0.002s FAILED (failures=2)
Most ideje megírni a from_roman()
függvényt.
def from_roman(s):
"""római számok egésszé alakítása"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral: ①
result += integer
index += len(numeral)
return result
to_roman()
függvénynél. Végiglépkedsz a római számok adatszerkezetén (tuple-k tuple-je), de a legmagasabb értékek lehető leggyakoribb illesztése helyett a „legmagasabb” értékű római számot képviselő karakterláncot illeszted olyan gyakran, amennyire csak lehetséges.
Ha nem teljesen világos, hogyan működik a from_roman()
, akkor adj egy print
utasítást a while
ciklus végéhez:
def from_roman(s):
"""római számok egésszé alakítása"""
result = 0
index = 0
for numeral, integer in roman_numeral_map:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
print('Megtalálva:', numeral, 'hossza:', len(numeral), ', hozzáadva:', integer)
>>> import roman5 >>> roman5.from_roman('MCMLXXII') Megtalálva: M hossza: 1, hozzáadva: 1000 Megtalálva: CM hossza: 2, hozzáadva: 900 Megtalálva: L hossza: 1, hozzáadva: 50 Megtalálva: X hossza: 1, hozzáadva: 10 Megtalálva: X hossza: 1, hozzáadva: 10 Megtalálva: I hossza: 1, hozzáadva: 1 Megtalálva: I hossza: 1, hozzáadva: 1 1972
Ideje újrafuttatni a teszteket.
you@localhost:~/diveintopython3/examples$ python3 romantest5.py ....... ---------------------------------------------------------------------- Ran 7 tests in 0.060s OK
Két izgalmas hír van itt. Az első, hogy a from_roman()
függvény jó bemenet esetén működik, legalábbis az összes ismert érték esetén. A második, hogy a „körbejárás” teszt is sikeres volt. Az ismert értékek teszttel kombinálva meglehetősen biztos lehetsz abban, hogy mind a to_roman()
, mind a from_roman()
függvény megfelelően működik az összes lehetséges jó értékre. (Erre nincs garancia, elméletileg előfordulhat, hogy a to_roman()
olyan programhibát tartalmaz, amely hibás római számokat állít elő bemenetek bizonyos halmazához, és hogy a from_roman()
olyan inverz programhibát tartalmaz, amely ugyanezeket a hibás egész értékeket állítja elő pontosan ugyanazon római számok halmazához, mint amelyeket a to_roman()
helytelenül állított elő. Az alkalmazástól és a követelményektől függően ez a lehetőség gondot okozhat, ebben az esetben írj átfogóbb teszteseteket, amíg a probléma meg nem szűnik.
⁂
Most hogy a from_roman()
függvény megfelelően működik jó bemenet esetén, ideje helyére illeszteni a kirakós utolsó darabját: tegyük megfelelően működővé rossz bemenettel is. Ehhez olyan módszert kell találnunk, amellyel egy karakterláncról ránézésre megállapítható, hogy érvényes római számok-e. Ez eredendően nehezebb, mint a numerikus bemenet ellenőrzése a to_roman()
függvényben, de rendelkezésedre áll egy hatékony eszköz: a reguláris kifejezések. (Ha nem ismered a reguláris kifejezéseket, akkor itt az ideje elolvasni a reguláris kifejezések fejezetet.)
Amint az Esettanulmány: római számok fejezetben láthattad, néhány egyszerű szabály vonatkozik a római számok előállítására az M
, D
, C
, L
, X
, V
és I
betűk használatával. Tekintsük át a szabályokat:
I
= 1
, az II
= 2
és az III
= 3
. A VI
= 6
(szó szerint „5
és 1
”), a VII
= 7
és a VIII
= 8
.
I
, X
, C
és M
) legfeljebb háromszor ismételhetők. A 4
esetén azt a következő ötös karakterből kell kivonnod. A 4
nem ábrázolható IIII
-ként; ehelyett a IV
használatos(„1
-gyel kisebb, mint 5
”). A 40
= XL
(„10
-zel kevesebb, mint 50
”), 41
= XLI
, 42
= XLII
, 43
= XLIII
és a 44
= XLIV
(„10
-zel kevesebb, mint 50
, és 1
-gyel kevesebb, mint 5
”).
9
esetén például a tőle nagyobb legelső tizes karakterből kell kivonnod: a 8
= VIII
, de a 9
= IX
(„1
-gyel kevesebb, mint 10
”), nem VIIII
(mivel az I
karakter nem ismételhető négyszer). A 90
= XC
, a 900
= CM
.
10
mindig X
-ként van ábrázolva, soha nem VV
-ként. A 100
mindig C
, soha nem LL
.
DC
= 600
; a CD
egy teljesen különböző szám (400
, „100
-zal kevesebb, mint 500
”). A CI
= 101
; az IC
nem is érvényes római szám (mert nem vonhatsz ki 1
-et közvetlenül a 100
-ból; 99 =XCIX
, „10
-zel kisebb, mint 100
, majd 1
-gyel kisebb, mint 10
”).
Emiatt egy hasznos tesztnek biztosítania kell, hogy a from_roman()
függvény nem fut le, ha egy túl sok ismétlődő karaktert tartalmazó karakterláncot kap. Hogy mennyi a „túl sok”, az a római számtól függ.
class FromRomanBadInput(unittest.TestCase):
def test_too_many_repeated_numerals(self):
'''a from_roman nem engedélyezhet túl sok ismétlődő karaktert'''
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
Egy másik hasznos teszt lehet annak ellenőrzése, hogy bizonyos minták nem ismétlődnek. Az IX
például 9
, de az IXIX
soha nem érvényes.
def test_repeated_pairs(self):
'''a from_roman nem engedélyezhet ismétlődő számpárokat'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
Egy harmadik teszt ellenőrizhetné, hogy a római számjegyek a helyes sorrendben jelennek-e meg, a legmagasabbtól a legalacsonyabb értékig. A CL
például 150
, de az LC
soha nem érvényes, mert az 50
-nek megfelelő karakter soha nem állhat a 100
-nak megfelelő előtt. Ez a teszt tartalmazza érvénytelen előtagok véletlenül kiválasztott halmazát: I
az M
előtt, V
az X
előtt, stb.
def test_malformed_antecedents(self):
'''a from_roman nem engedélyezhet rosszul formázott előtagokat'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
Ezen tesztek mindegyike arra épül, hogy a from_roman()
függvény egy új, InvalidRomanNumeralError
kivételt dob, ezt azonban még nem definiáltuk.
# roman6.py
class InvalidRomanNumeralError(ValueError): pass
A három teszt egyike sem lehet sikeres, mert a from_roman()
függvény még egyáltalán nem tartalmaz érvényesség-ellenőrzést. (Ha sikeresek lennének, akkor mi a fenét tesztelnének?)
you@localhost:~/diveintopython3/examples$ python3 romantest6.py FFF....... ====================================================================== FAIL: test_malformed_antecedents (__main__.FromRomanBadInput) a from_roman nem engedélyezhet rosszul formázott előtagokat ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest6.py", line 113, in test_malformed_antecedents self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ====================================================================== FAIL: test_repeated_pairs (__main__.FromRomanBadInput) a from_roman nem engedélyezhet ismétlődő számpárokat ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest6.py", line 107, in test_repeated_pairs self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ====================================================================== FAIL: test_too_many_repeated_numerals (__main__.FromRomanBadInput) a from_roman nem engedélyezhet túl sok ismétlődő karaktert ---------------------------------------------------------------------- Traceback (most recent call last): File "romantest6.py", line 102, in test_too_many_repeated_numerals self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s) AssertionError: InvalidRomanNumeralError not raised by from_roman ---------------------------------------------------------------------- Ran 10 tests in 0.058s FAILED (failures=3)
Jókora adag. Most csak annyit kell tennünk, hogy a római számok érvényességét tesztelő reguláris kifejezést hozzáadjuk a from_roman()
függvényhez.
roman_numeral_pattern = re.compile(''' ^ # karakterlánc eleje M{0,3} # ezresek - 0 és 3 közti M (CM|CD|D?C{0,3}) # százasok - 900 (CM), 400 (CD), 0-300 (0 és 3 közti C), # vagy 500-800 (D, amelyet 0 és 3 közti C követ) (XC|XL|L?X{0,3}) # tizesek - 90 (XC), 40 (XL), 0-30 (0 és 3 közti X), # vagy 50-80 (L, amelyet 0 és 3 közti X követ) (IX|IV|V?I{0,3}) # egyesek - 9 (IX), 4 (IV), 0-3 (0 és 3 közti I), # vagy 5-8 (V, amelyet 0 és 3 közti I követ) $ # karakterlánc vége '''def from_roman(s): '''római számok egésszé alakítása''' if not roman_numeral_pattern.search(s): raise InvalidRomanNumeralError('Érvénytelen római szám: {0}'.format(s)) result = 0 index = 0 for numeral, integer in roman_numeral_map: while s[index : index + len(numeral)] == numeral: result += integer index += len(numeral) return result
És futtasd újra a teszteket…
you@localhost:~/diveintopython3/examples$ python3 romantest7.py .......... ---------------------------------------------------------------------- Ran 10 tests in 0.066s OK
Az év ellencsúcspontja díjat pedig… az „OK
” szó kapja, amelyet a unittest
modul ír ki, ha az összes teszt sikerül.
© 2001–11 Mark Pilgrim