Spomnimo se naloge Kaladont. Po par tednih smo toliko pametnejši, da znamo z uporabo slovarjev, množic, izpeljanih slovarjev, generatorjev in različnih drugih trikov večino funkcij napisati veliko krajše. Točneje, vse te funkcije je možno napisati tako, da vsebujejo en sam return in nič drugega.

V nalogi tudi ne smeš definirati nobenih drugih funkcij kot te, ki jih zahteva naloga.

Testi

Testi: testi-kaladont-na-hitro.py

Obvezna naloga

Napiši enovrstične različice funkcij

  • lahko_sledi(prej, potem)
  • ni_ponavljanj(besede)
  • preveri_zaporedje(besede)
  • mozne_naslednje(beseda, slovar)

Rešitev

Kot so nekateri opazili, sem rešitve te naloge že praktično objavil skupaj z (daljšimi) rešitvami prvotne naloga Kaladont. Razmišljal sem, da s tem ni nič narobe, če to pomeni, da bo rešitve naloge prebral kdo, ki tega sicer ne bi storil. In če ga bo to spodbudilo k temu, da bo to počel tudi v prihodnje.

def lahko_sledi(prej, potem):
    return prej[-1] == potem[0] and prej != potem

def ni_ponavljanj(besede):
    return len(set(besede)) == len(besede)

def preveri_zaporedje(besede):
    return ni_ponavljanj(besede) and \
           all(lahko_sledi(prej, potem) for prej, potem in zip(besede, besede[1:]))

def mozne_naslednje(beseda, slovar):
    return [potem for potem in slovar if lahko_sledi(beseda, potem)]

Funkcija lahko_sledi ni nič takšnega, česar ne bi morali znati že, odkar znamo napisati funkcijo.

Pri ni_ponavljanj so se nekateri spomnili na množice, drugi so pisali reči v slogu return all(besede.count(beseda) == 1 for beseda in besede). Tudi prav; so pač ponovili count in se spomnili na all...

... ki nam pride prav pri preverjanju zaporedja. Tu očitno najprej pokličemo funkcijo ni_ponavljanj, nato pa preverimo, ali za vsak par besed prej in potem (for prej, potem in zip(besede, besede[1:])) velja, da lahko_sledi(prej, potem).

Tisti, ki se niso spomnili na all, so navadno sestavili seznam Truejev in Falseov, lahko_sledi(prej, potem) for prej, potem in zip(besede, besede[1:]), potem pa niso vedeli, kaj z njim. Lahko ga uporabimo, recimo, tako

def preveri_zaporedje(besede):
    return ni_ponavljanj(besede) and \
           False not in [lahko_sledi(prej, potem) for prej, potem in zip(besede, besede[1:])]

Deluje, ampak z all je lepše.

O mozne_naslednje pa se spet nimamo kaj dosti pogovarjati.

Dodatna naloga

Napiši enovrstične različice funkcij

  • izberi_besedo(beseda, slovar)

    Tu iščemo besedo, ki je v določenem smislu najboljša, torej minimalna ali maksimalna po določenem kriteriju. Preuči dokumentacijo za funkciji max in min. Za lažje razumevanje lahko pogledaš še nekaj o urejanju, vendar ne uporabljaj funkcije sort, saj je ne potrebuješ.

    Pri programiranju smeš uporabiti funkcijo kvaliteta_besede, ki se nahaja v testih. Dobro si jo oglej in jo razumi.

  • pogostosti_zacetnic(slovar)

    Od tvoje rešitve ne pričakujemo, da bo zelo hitra (seveda pa se bo tudi slaba funkcija pri tako kratkih testih izvedla v trenutku).

Rešitev

Kdor je reševal izberi_besedo in pri tem uporabil kvaliteta_besede, se je naučil, kako urejati ali pa izbirati največji oz. najmanjši element po več kot enem kriteriju.

Funkcija max vrne največji element seznama. Seznam lahko vsebuje karkoli, kar je mogoče primerjati. Če vsebuje nize, bo vrnil tistega, ki je največji - po kriteriju, po katerem operator < primerja nize. To seveda ni dolžina (čemu bi to služilo?), temveč operator < primerja nize po abecedi. Tako bi funkcija max(slovar) bi vrnila besedo v slovarju, ki je zadnja po abecedi.

Kaj pa, če bi hoteli poiskati najdaljšo besedo v slovarju? Za take potrebe ima max dodaten argument, ki ga moramo podati z imenom. Poklicati moramo max(slovar, key=len). len je, vemo, funkcija, ki vrne dolžino niza. Funkcija max bo za vsak element seznama poklicala podano funkcijo len in vrnila tisti element, pri katerem len vrne največjo vrednost.

Pa če bi hoteli poiskati besedo z največ a-ji? Funkcije, ki bi vrnila število a-jev v besedi, ni, seveda pa jo lahko napišemo.

def stevilo_ajev(beseda):
    return beseda.count("A")

print(max(slovar, key=stevilo_ajev)

Če bi hoteli funkcijo z največ e-ji, pišemo, seveda,

def stevilo_ejev(beseda):
    return beseda.count("E")

print(max(slovar, key=stevilo_ejev)

Pa če hočemo tako z največ a-ji, a ker je besed z enakim (največjim) številom a-jev več, bomo med njimi izbrali tisto z največ e-ji? Tu si pomagamo s terkami in tem, kako Python primerja terke: dve terki primerja po prvem elementu, če je ta enak, pa po drugem (in če bi bil enak tudi ta, po tretjem, in tako naprej).

def stevilo_ajev_in_ejev(beseda):
    return (beseda.count("A"), beseda.count("E"))

print(max(slovar, key=stevilo_ajev_in_ejev)

Kaj pa, če bi med besedami z enakim številom a-jev hoteli tisto z najmanj e-ji? Zdaj imamo problem, saj kličemo funkcijo max, ne min -- saj hočemo največ a-jev.

Problem rešimo z malo zvitosti

def stevilo_ajev_in_ejev(beseda):
    return (beseda.count("A"), -beseda.count("E"))

Pred beseda.count("E") dodamo minus, pa bo iskal tistega z največ minus E-ji, se pravi tistega z najmanj E-ji.

Naloga zahteva, da poiščemo besedo, ki je najdaljša, med enako dolgimi pa tisto, ki je prej po abecedi. Če bi imeli funkcijo

def kvaliteta_besede(beseda):
    return (len(beseda), beseda)

in z njo poklicali max, bi dobili najdaljšo besedo, med njimi pa tisto, ki je zadnja po abecedi. Problema ne moremo rešiti tako kot zgoraj, kjer smo negirali število E-jev. beseda je niz in je ne moremo "negirati". Zato trik obrnemo. Iščemo minimum, funkcija, s katero si pomagamo, pa ima negirano dolžino -- to je natančno funkcija, ki je bila priložena navodilom (oz. testom):

def kvaliteta_besede(beseda):
    return (-len(beseda), beseda)

Rešitev naloge je torej

def izberi_besedo(beseda, slovar):
    return min(mozne_naslednje(beseda, slovar), key=kvaliteta_besede)

Če se ne gremo teh trikov s key je rešitev malo grša.

def izberi_besedo(beseda, slovar):
    return min(mozna for mozna in mozne_naslednje(beseda, slovar)
               if len(mozna) == max(len(b) for b in mozne_naslednje(beseda, slovar)))

Poiščemo prvo besedo po abecedi min med tistimi besedami izmed možnih naslednjih (for mozna in mozne_naslednje(beseda, slovar)), ki so dolge toliko, kolikor je daljša najdaljša beseda izmed možnih naslednjih (if len(mozna) == max(len(b) for b in mozne_naslednje(beseda, slovar))). Ta funkcija je seveda grozna: za vsako besedo na novo išče dolžino najdaljše besede, pri čemer mora še vsakič na novo gledati, katere so možne naslednje besede.

Ekvivalentna rešitev je

def izberi_besedo(beseda, slovar):
    kandidati = mozne_naslednje(beseda, slovar)
    naj_dolzina = max(len(kandidat) for kandidat in kandidati)
    return min(mozna for mozna in kandidati if len(mozna) == naj_dolzina)

Z njo ni nič zelo narobe, le testov ne prestane, ker zahtevajo, da vse opravimo v eni vrstici in ne treh. A če boste programirali tako, vam ne bo nihče zameril.

Tudi pogostosti začetnic so vam dale vetra -- in to poučnega. Veliko vas je poskušalo najprej sestavite seznam in ga potem nekako spreminjati. A čim napišemo [0] * 25 (ali, hujše, [0, 0, 0, 0, ... in tako naprej]), smo dobili seznam petindvajsetih ničel, ki jih ne bo nihče več spremenil v kaj drugega. V tovrstnih programih pozabite na +=, tega ni. Ti programi delajo le s konstantami in ne morejo ničesar spreminjati.

Enako so se zapletli tisti, ki so si predstavljali, da bo potrebno iti čez slovar. Čim napišemo [... for beseda in slovar] bomo dobili seznam, ki ima toliko elementov, kolikor je v slovarju besed.

Razmišljati moramo začeti s prave strani. Če želimo slovar s 25 elementi, ki bodo ustrezali črkam slovenska abecede, potrebujemo zanko čez 25 stvari, to je, čez 25 črk slovenske abecede. Torej [... for c in "ABCČDEFGHIJKLMNOPRSŠTUVZŽ"]

Zdaj pa moramo ... zamenjati z nekim izrazom, ki bo preštel, koliko besed se začne s črko c. Seznam takšnih besed menda znamo sestaviti: [beseda for beseda in slovar if beseda[0] == c]. Dolžina tega seznama pa je prav to, kar iščemo.

def pogostosti_zacetnic(slovar):
    return [len([beseda for beseda in slovar if beseda[0] == c])
            for c in "ABCČDEFGHIJKLMNOPRSŠTUVZŽ"]

Lahko pa se spomnimo, da je True isto kot 1 in False isto kot 0. Sestavimo seznam True-jev in False-ov, ki bodo povedali ali se beseda v slovarju začne s pravo črko ali ne. Nato ga seštejemo.

def pogostosti_zacetnic(slovar):
    return [sum(beseda[0] == c for beseda in slovar)
            for c in "ABCČDEFGHIJKLMNOPRSŠTUVZŽ"]