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.

Zadnja sprememba: četrtek, 5. oktober 2017, 11.49