Zapiski (2015/16)
Testi
Pri predmetu boste dobivali tedenske domače naloge. Skoraj vedno jih bodo spremljali testi. Z njimi boste lahko že kar sami preverili, ali je vaša rešitev pravilna. Z njimi bom poskusil tudi pokriti tipične napake, ki bi jih lahko storili pri programiranju (kolikor jih bom pač uganil). Koristni bodo tudi, ker bodo dopolnili navodila: če se bo zdel kak del navodil dvoumen (morda zato, ker tudi v resnici je), si lahko pomagate s testi.
Na izpitu ste reševali nalogo, ki je zahtevala, da napišete funkcijo
anagram(a, b), ki vrne True, če sta besedi a in b anagrama, in False,
če nista.
Test je videti, recimo, takole
import unittest
class TestAnagrami(unittest.TestCase):
def test_anagram(self):
self.assertTrue(anagram("tipka", "pikat"))
self.assertFalse(anagram("tipka", "pirat"))
def test_se_en_test(self):
self.assertFalse(anagram("tipka", "piikat"))
self.assertFalse(anagram("tippka", "piikat"))
if __name__ == "__main__":
unittest.main()
Test kliče vašo funkcijo z različnimi argumenti in preverja, ali naredi,
kar naj bi naredila. Tile testi so karseda preprosti: pričakujejo, da bo
funkcija vrnila True, če je pokličemo z anagram("tipka", "pikat") in
da bo vrnila False, če jo pokličemo anagram("tipka", "pirat") ali
anagram("tipka", "piikat").
Testi so razdeljeni v dva dela - en del se imenuje test_anagram, drugi
test_se_en_test. Tokrat za to ni posebnega razloga, včasih pa bodo v
resnici testirali različne funkcije ali različne lastnosti iste funkcije.
Napišimo, za vajo, neumno rešitev: funkcijo, ki vrne True, če tretja
črka niza ni enaka i.
def anagram(a, b):
return b[2] != "i"
Funkcijo napišemo nad teste. Teste najpreprosteje poženemo tako, da
pustimo kurzor izven testov in pritisnemo Ctrl-Shift-F10 (na Macu pa
Cmd-Shift-R). Če je kurzor znotraj testov, pa bo ta kombinacija pognala
testno funkcijo, v kateri je kurzor (test_anagram ali test_se_en_test).
Ko torej poženemo test, dobimo:
Oranžne "lučke" pomenijo, da program ne deluje pravilno. Rdeča bi pomenila, da se je sesul z napako. Zelena pomeni, da je vse v redu.
Ne pozabite pogledati sporočila o napaki. Takole pravi:
Failure
Traceback (most recent call last):
File "/Users/janezdemsar/Dropbox/Pedagosko/P1 - PeF/01 - ponovitev/testi.py", line 11, in test_anagram
self.assertFalse(anagram("tipka", "pirat"))
AssertionError: True is not false
Iz predzadnjih dveh vrstic vidimo, na kakšen način funkcija ne deluje:
ko pokličemo anagram("tipka", "pirat") pričakujemo rezultat False,
funkcija pa vrne True, kar je narobe (True is not false).
Tole, zadnje sporočilo zveni malo smešno. Tako je, ker govori o "false"
in "true". Testi bodo pogosto izpisovali drugačna sporočila: imeli bomo,
recimo, funkcijo, od katere bomo pričakovali, recimo, seznam,
[1, 2, 3]. Testirali bi jo, recimo, takole:
self.assertEqual(vrni123(arg1, arg2), [1, 2, 3])
Torej, če pokličemo funkcijo vrni123 z argumentoma arg1 in arg2,
mora vrniti [1, 2, 3]. Druge "asserte" bomo spoznali sproti, ko jih
bomo potrebovali.
PyCharm včasih ne odkrije, da gre za datoteko s testi in jo namesto tega požene, kot da bi bila običajen program. V tem primeru ne dobite "semaforjev", vseeno pa dobite sporočila o napakah, kar zadošča. Če želite lučke, pa lahko uporabite rešitev, ki jo je predlagala Iva (hvala!)
Na projektu ob desnem kliku izberite New/Python file, ter po "Name" spodaj, kjer piše "Kind: Python file", namesto "Python file" izberite "Python unit test". Odprl se bo nov zavihek, v katerem bo že neko besedilo. Tisto pobrišite in v to datoteko kopirajte svojo kodo s testi profesorja, nato bi, ko desno kliknete na zavihek, moralo namesto samo "Run (in ime datoteke)" pisati "Run Unittests in (ime datoteke)".
(Testi profesorja zveni dvoumno. Je to nekaj takega kot v soboto si lahko ogledate streljanje rezervnih oficirjev?)
Anagrami
Preštevanje
Prvi program, ki smo ga napisali, je bil takšen.
def anagram(a, b):
if len(a) != len(b):
return False
for crka in a:
if crka not in b:
return False
return True
Najprej smo preverili, ali sta besedi enako dolgi. Če nista, je veselja konec.
Sicer za vsako črko prve besede preverimo, ali se pojavi tudi v drugi. Če se
ne, takoj vrnemo False. Sicer preverjamo naprej in šele, ko pridemo do konca
besede a, vrnemo True.
Pogosta napaka (ki je na predavanjih niste in niste hoteli narediti :), je
def anagram(a, b):
if len(a) != len(b):
return False
for crka in a:
if crka not in b:
return False
else:
return True
Če naredimo tako, funkcija že po prvi črki vrne True, če jo najde v drugi
besedi. Ostalih črk sploh ne gleda.
No, čeprav smo se tej napaki izognili, se testi končajo z
self.assertFalse(anagram("tippka", "piikat"))
AssertionError: True is not false
Vse črke iz a se pojavijo v b - vendar ne tolikokrat, kot v a-ju.
Popravimo torej funkcijo tako, da bo preverjala število ponovitev.
def anagram(a, b):
if len(a) != len(b):
return False
for crka in a:
if a.count(crka) != b.count(crka):
return False
return True
Tako je pa v redu.
Na predavanjih sem nato (zmotno!) rekel, da preverjanje dolžin besed zdaj niti ni več potrebno in lahko napišemo kar
def anagram(a, b):
for crka in a:
if a.count(crka) != b.count(crka):
return False
return True
Ni res. Testi sicer ne najdejo napake, a le zato, ker takrat, ko sem sestavljal test, nisem pomislil na to napako. Če med teste dodamo še
self.assertFalse(anagram("tipka", "pikatx"))
pa funkcija anagram (brez preverjanja dolžin) reče, da gre za anagrama, saj
se vsaka črka a-ja enakokrat pojavi v b. Žal pa ima b še eno odvečno
črko.
Črtanje
Druga ideja je, da gremo prek črk a-ja in "črtamo" črke iz b-ja.
Nizov ne moremo spreminjati. Iz niza, ki je "shranjen" v b-ju, ne moremo
pobrisati črke. Pač pa lahko sestavimo nov niz in ga shranimo v b. Tako nismo
spremenili niza b, pač pa se b nanaša na nov niz. (Izgleda kot igra besed.
Ni. Bomo videli, ko pride čas.)
def anagram(a, b):
for crka in a:
if crka not in b:
return False
u = b.index(crka)
b = b[:u] + b[u + 1:]
return b == ""
Gremo prek vse črk a-ja. Če te črke ni (ali ni več) v b-ju, vrnemo
False. Sicer ugotovimo, kje v b-ju je ta črka; indeks shranimo v u
in sestavimo nov niz tako, da vzamemo vse črke do u-te in vse od
u+1-te naprej.
Na koncu v b-ju ne sme ostati nič več. Morda bi koga zamikalo napisati
if b == "":
return True
else:
return False
To sicer deluje, vendar ... nima smisla. Izraz b == "" že ima vrednost
True ali False. In prav to vrednost hočemo vrniti. Zato zadošča
return b == "". Kdor motovili iz if-om, najbrž ne ve, kaj pomeni b == "".
Ker so vse prazne stvari neresnične, bi lahko pisali tudi kar return not b.
Funkcija mora vrniti True natanko takrat, ko je b prazen, neresničen.
Nizov ne moremo spreminjati, pač pa lahko spreminjamo sezname. Oba niza pretvorimo v seznama, pa bomo lahko brisali brez zafrkavanja z indeksi.
def anagram(a, b):
a = list(a)
b = list(b)
for crka in a:
if crka not in b:
return False
b.remove(crka)
return not b
Nekdo je modro pripomnila, da a-ja ni potrebno spreminjati v seznam.
Bravo, drži. Prva vrstica, a = list(a), je odveč.
Urejanje
Imenitna funkcija sorted sprejme vsako reč, prek katere bi se dalo iti
z zanko for, in vrne seznam urejenih elementov te reči. Elementi niza
so črke, torej bo sorted(a) vrnil urejen seznam črk a-ja, sorted(b)
pa urejen seznam črk b-ja. Če sta seznama enaka, sta besedi anagrama.
def anagram(a, b):
return sorted(a) == sorted(b)
Slovarji
Tega pa še ne znamo. Vse, kar smo delali zgoraj, je neučinkovito, saj večkrat
preiskuje b, ali pa ureja elemente besed. Če bi imeli dolge (res dolge!)
besede s tisoči in deset tisoči znakov, bi se to poznalo. Prihodnji teden se
bomo zato naučili boljšega, hitrejšega načina za preverjanje anagramov.