Testi

Testi: testi-maraton.py

Podatki: 10z.txt (dodaj v isti direktorij kot program)

Ogrevalne vaje

Napiši funkcijo v_sekunde(s), ki prejme čas kot niz, ki vsebuje ure, minute in sekunde, ločene z dvopičjem (na primer "1:02:15" ali "1:2:15" ali "01:2:15"...). Vrniti mora čas v sekundah; v gornjem primeru vedno vrne 3735.

Napiši funkcijo iz_sekund(s), ki dobi čas v sekundah in vrne niz. Če pokličemo iz_sekund(3735), naj vrne "1:2:15".

Napiši funkcijo podatki(s), ki prejme rezultat enega udeleženca maratona, v obliki "1 14895 ROMAN SONJA 1979 SLO 0:16:10 0:32:20". Podatki v tej vrstici so uvrstitev, štartna številka, ime in priimek, letnica rojstva, država, vmesni čas in končni čas. Podatki so ločeni s tabulatorjem, zato vrstico razcepite s split("\t"). (Kaj je \t, se bomo še učili - za zdaj samo uporabite takšen klic metode split, pa bo.) Funkcija naj vrne štiri stvari in sicer ime in priimek, leto rojstva (kot število, ne niz), vmesni čas v sekundah in končni rezultat v sekundah.

Rešitev

Ogrevalne naloge zahtevajo uporabo split in join. Pri prvi je potrebno razbiti niz glede na dvopičje. Dele, ki jih dobimo, bomo poimenovali h, m in s, kar pomeni Her Majesty's Ship. Spremenili jih bomo v int, ustrezno pomnožili in sešteli.

def v_sekunde(s):
    h, m, s = s.split(":")
    return int(h) * 3600 + int(m) * 60 + int(s)

Alternativa bi bila, h = s[:2], m = s[3:5] in s = s[-2]. Vendar to deluje le, če so vsa števila napisana z vodilno ničlo (03:20:05), če niso (3:20:5), pa ne. Tu namerno niso bila, da ste morali uporabiti split.

Zdaj pa iz_sekund. Če imamo število sekund, dobimo ure s celoštevilskim deljenjem s 3600. Minute dobimo tako, da sekunde celoštevilsko delimo s 60, potem pa vzamemo ostanek po deljenju s 60; kar je več, so že ure. Ostanek sekund dobimo tako, da izvirne sekunde delimo s 60.

Te reči potem združimo z dvopičjem. Oziroma, dvopičju rečemo, naj združi te reči, ki jih prej pretvorimo v nize.

def iz_sekund(s):
    h = s // 3600
    m = (s // 60) % 60
    s = s % 60
    return ":".join([str(h), str(m), str(s)])

Zadnjo vrstico bi lahko zamenjali z return str(h) + ":" + str(m) + ":" + str(s). Kaj je boljše? V tem primeru nič: obstaja še spodobnejši način, vendar ga še ne poznamo.

(Za tiste, ki itak prehitevajo povejmo: starejši način je return "%02i:%02i:%02i" % (h, m, s), priporočeni pa "{:02}:{:02}:{:02}".format(h, m, s).)

Podatke je potrebno, ko pravi naloga, razdeliti s split("\t"). Potem le še razpakiramo. V returnu mimogrede spremenimo leto v število in čase v sekunde.

def podatki(s):
    mesto, stevilka, ime, leto, drzava, cas1, cas2 = s.split("\t")
    return ime, int(leto), v_sekunde(cas1), v_sekunde(cas2)

Obvezni del

Napiši funkcijo pospesek(vrstice), ki prejme vrstico, kakršno smo opisali zgoraj. Vrne naj razmerje med časom, ki ga je tekač potreboval za drugi del proge in časom, ki ga je potreboval za prvi del. Če je potreboval za drugi del 30 minut, za prvega pa 20 minut, naj vrne 30 / 20.

Napiši funkcijo naj_pospesek(vrstice), ki prejme seznam vrstic, kakršne smo opisali zgoraj. (Ehm. V resnici ne bo seznam, zato ga ne poskušaj indeksirati z oglatimi oklepaji. Kar lepo uporabi znanko for!) Vrne naj ime in priimek tistega tekača, ki je najbolj pospešil v drugem delu proge.

Napiši funkcijo vsi_pospeseni(vrstica, faktor), ki dobi seznam vrstic in "faktor pospeška". Vrne naj seznam imen vseh tekmovalcev, ki so pospešili za tak faktor. Torej: če je faktor enak 0.8, naj vrne vse, pri katerih je bil cas v drugem delu proge enak ali manjši kot 0.8 krat čas v prvem delu. Takšen pospešek imajo, na primer, tisti, ki so prvi del pretekli v 20 minutah, drugega pa v 0.8 * 20 = 16 minutah. Ali pa prvi del v 60 minutah in drugi del v 0.8 * 60 = 48 minutah.

Napiši funkcijo leta(vrstice), ki vrne urejen seznam rojstnih letnic tekmovalcev. Vsaka letnica naj se pojavi le enkrat, tudi če je bilo tega leta rojenih več tekačev. Rezultat, če uporabimo resnične podatke za ženske, ki so na Ljubljanskem maratonu tekle na 10 km, je [1937, 1938, 1942, 1943, 1946, 1947, 1948 ... in tako naprej.

Napiši funkcijo tekaci_leta(vrstice, leto), ki vrne niz z imeni vseh tekačev, rojeni podanega leta. Niz naj bo oblike "BOLE JOŽICA, JAGER JOŽICA, KOČEVAR MILA in RUPAR ALENKA".

Rešitev

Pospešek pokliče funkcijo, ki razbere podatke in vrne zahtevani kvocient. Upoštevati moramo, da je cas2 končni čas, torej je čas, ki ga je tekač potreboval za drugi del proge, enak cas2 - cas1.

def pospesek(vrstica):
    ime, leto, cas1, cas2 = podatki(vrstica)
    return (cas2 - cas1) / cas1

naj_pospesek je spet čisto običajno iskanje največjega elementa, ki ga ne bomo stotič opisoval. Devetindevetdesetkrat je bilo dovolj.

def naj_pospesek(vrstice):
    naj_raz = 1
    naj_ime = None
    for vrstica in vrstice:
        raz = pospesek(vrstica)
        if raz < naj_raz:
            naj_ime = podatki(vrstica)[0]
            naj_raz = raz
    return naj_ime

Ko iz vrstice poberemo ime (da ga spravimo v naj_ime) ne uporabljamo razpakiranja temveč indeks. Preprosto zato, ker ne potrebujemo drugega kot ime. Nikoli nisem rekel, da so indeksi vedno prepovedani.

Ker funkcije po natančno tem vzorcu stalno pišemo, povejmo, da se bomo nekoč naučili imenitno bližnjico. Takole gre: celo gornjo funkcijo lahko zamenjamo z

def naj_pospesek(vrstice):
    return podatki(min(vrstice, key=pospesek))[0]

Funkcija vsi_pospeseni so drug klasičen vzorec. Tako kot za iskanje največjega elementa bomo spoznali krajšo pot, za zdaj pa gre tako: naredimo prazen seznam (pospeseni = []), gremo čez vrstice (for vrstica in vrstice) in za vsako, ki ustreza pogoju (if pospesek(vrstica) <= f) dodamo ime v seznam (pospeseni.append(podatki(vrstica)[0])).

def vsi_pospeseni(vrstice, f):
    pospeseni = []
    for vrstica in vrstice:
        if pospesek(vrstica) <= f:
            pospeseni.append(podatki(vrstica)[0])
    return pospeseni

Spet povejmo, da se bomo nekoč naučili bližnjico. Gornjo funkcijo lahko sprogramiramo tudi takole

def vsi_pospeseni(vrstice, f):
    return [podatki(vrstica)[0] for vrstica in vrstice if pospesek(vrstica) <= f]

Da sprogramiramo leta, moramo znati preverjati, ali je neko leto že v seznamu. To storimo z operatorjem in. Saj se ga spomnite s predavanj? Spet naredimo prazen seznam, gremo prek vseh vrstic in vsako leto, ki ga še ni v seznamu, dodamo v seznam. Nato seznam uredimo in vrnemo.

def leta(vrstice):
    vsa_leta = []
    for vrstica in vrstice:
        ime, leto, cas1, cas2 = podatki(vrstica)
        if not leto in vsa_leta:
            vsa_leta.append(leto)
    vsa_leta.sort()
    return vsa_leta

Napaka, ki ste jo mnogi delali tule, je return vsa_leta.sort(). Metoda sort uredi seznam kar "na mestu" in ne vrača ničesar. Če torej napišete return vsa_leta.sort(), boste vrnili None.

Pač pa obstaja funkcija sorted, ki prejme poljubno reč, ki jo lahko spremeni v seznam, in jo sortira. Namesto

    vsa_leta.sort()
    return vsa_leta

lahko pišemo

    return sorted(vsa_leta)

Bližnjica? Seveda.

def leta(vrstice):
    return sorted({podatki(vrstica)[1] for vrstica in vrstice})

Funkcija tekaci_leta se začne z istim vzorcem: naredimo prazen seznam, in vanj zložimo imena vseh tekačev, ki ustrezajo določenemu pogoju. Ostanek funkcije previdno sestavi niz. Če ni tekačev, vrne prazen seznam. Če je le eden, vrne njegovo ime. Če jih je več, združi vse razen zadnjega z vejico, nato doda "in" in doda še zadnjega tekača. Tako kot sem pokazal na predavanjih.

def tekaci_leta(vrstice, leto):
    tekaci = []
    for vrstica in vrstice:
        ime, leto1, cas1, cas2 = podatki(vrstica)
        if leto == leto1:
            tekaci.append(ime)
    if not tekaci:
        return ""
    if len(tekaci) == 1:
        return tekaci[0]
    return ", ".join(tekaci[:-1]) + " in " + tekaci[-1]

Bližnjica? Eh, niti ne. Za prvi del že še nekako. Drug del pa je izživljanje. Napišimo, ampak ni nič posebej lepega.

def tekaci_leta(vrstice, leto):
    tekaci = [ime for ime, leto1, cas1, cas2 in (podatki(vrstica)
              for vrstica in vrstice) if leto1 == leto]
    return ", ".join(tekaci[:-1]) + " in " * (len(tekaci) > 1) + (tekaci[-1] if tekaci else "")

Častni krog (Dodatna naloga)

Napiši funkcijo najboljsi_po_letih(vrstice), ki vrne urejen seznam parov letnic rojstev in ime najboljšega tekača, rojenega tistega leta. Kako mora izgledati seznam, si lahko ogledaš v testih.

Rešitev te naloge je tako ogabna, da si jo moramo ogledati.

def najboljsi_po_letih(vrstice):
    po_letih = []
    for vrstica in vrstice:
        ime, leto, cas1, cas2 = podatki(vrstica)
        for i, (leto1, naj_ime, naj_cas) in enumerate(po_letih):
            if leto == leto1:
                if cas2 < naj_cas:
                    po_letih[i] = (leto, ime, cas2)
                break
        else:
            po_letih.append((leto, ime, cas2))

    po_letih2 = []
    for leto, ime, cas in po_letih:
        po_letih2.append((leto, ime))
    return sorted(po_letih2)

Seznam po_letih bo vseboval trojke (leto, ime, cas), pri čemer bo ime ime najhitrejšega (doslej najdenega) tekača, rojenega v tem letu in cas njegov čas.

Gremo čez vrstice. Pri vsakem tekaču gremo čez ves seznam po_letih, dokler ne najdemo elementa, ki se nanaša na tole leto. Če ne bi potrebovali indeksa, bi pisali for leto1, naj_ime, naj_cas in po_letih. Ker potrebujemo še indeks, dodamo enumerate in pišemo for i, (leto1, naj_ime, naj_cas) in enumerate(po_letih). Razpakirati moramo pač terko v terki, odtod dodatni oklepaji. Ta zanka se vrti, dokler ne najdemo leta, v katerem je rojen tekač iz trenutno opazovane vrstice (if leto == leto1). Če je njegov čas najboljši od najboljšega, zamenjamo ta element seznama s podatki o tem tekaču. V vsakem primeru pa zanko zaključimo z break; ker smo našli element, ki vsebuje najboljšega v tem letu, nima smisla iskati naprej. Pazite: brak je znotraj if leto == leto1.

Zanki for sledi else. Ta se bo izvedel, če se zanka ni prekinila z break. Ker jo z break prekinemo, čim najdemo leto, nam to, da se ni prekinila, očitno pove, da doslej nismo videli še nobenega tekača, rojenega v letu, v katerem je rojen tekač iz trenutne vrstice. V tem primeru ga dodamo.

Po teh mukah imamo seznam trojk (leto, ime, cas) za najboljše tekače v vsakem letu rojstva. Ker naloga zahteva, da vrnemo le leto in ime, predelamo seznam in ga vrnemo urejenega.

Grozno.

Bistveno praktičnejša rešitev je tale.

def najboljsi_po_letih(vrstice):
    po_letih = [("", 0)] * 2015
    for vrstica in vrstice:
        ime, leto, cas1, cas2 = podatki(vrstica)
        naj_ime, naj_cas = po_letih[leto]
        if not naj_ime or cas2 < naj_cas:
            po_letih[leto] = (ime, cas2)

    po_letih2 = []
    for leto, (ime, cas) in enumerate(po_letih):
        if ime:
            po_letih2.append((leto, ime))
    return po_letih2

po_letih je seznam, z 2016 elementi. po_letih[1956] bo vseboval ime in čas najhitrejšega tekača rojenega leta 1956. Nič ne de, da bodo nekateri elementi ostali prazni. V po_letih[1800] ne bo ničesar, ker je France že umrl.

Seznam sestavimo tako, da bo v začetku vseboval pare ("", 0); čas ni pomemben, pomembno je, da imamo prazen niz.

Zdaj gremo čez vse vrstice. Najprej preberemo podatke o tekaču, nato podatke o najboljšem tekaču iz tistega leta, naj_ime, naj_cas = po_letih[leto]. Če tekača ni ali pa če ima tekač iz trenutne vrstice boljši čas (if not naj_ime or cas2 < naj_cas), zamenjamo podatek za to leto s podatki o tem tekaču (po_letih[leto] = (ime, cas2)).

Na koncu to spet predelamo v nov seznam. Spet uporabimo enumerate; indeks zdaj predstavlja kar leto. V novi seznam prepišemo vse tiste elemente, ki imajo neprazno ime - torej tista leta, v katerih je bil rojen kateri od tekačev. Urejanje ni potrebno, saj je na ta način sestavljen seznam že urejen.

Ta rešitev je bila pravzaprav zelo dobra. Lahko jo še izboljšamo tako, da naredimo seznam s 116 elementi in od letnic odštejemo 1900. Boljše kot tako že skoraj ne gre.

Lepota te rešitve je v tem, da nam ni potrebno ničesar iskati, temveč - za razliko od prve rešitve - vedno že kar vemo, kje se nahaja določena reč.

Rešuje pa nas, da je število različnih letnih majhno - v najslabšem primeru bi jih bilo dobrih 100 (če tečejo tudi dojenčki in stoletniki). Če bi preštevali kaj drugega, kjer bi bilo število možnih števil bistveno večje, pa bi potrebovali gromozanski seznam. Ali celo (praktično) neskončno velik seznam, če bi bile številke realne.

V tem primeru - pa tudi v primeru, ki ga imamo v resnici - se splača uporabiti slovarje, o kateri se bomo, glej no, učili, prav na predavanjih, ki sledijo tej domači nalogi. Tule le povejmo rešitev.

def najboljsi_po_letih1(vrstice):
    po_letih = {}
    for vrstica in vrstice:
        ime, leto, cas1, cas2 = podatki(vrstica)
        if not leto in po_letih or (cas2, ime) < po_letih[leto]:
            po_letih[leto] = cas2, ime

    po_letih2 = []
    for leto in range(1900, 2015):
        if leto in po_letih:
            po_letih2.append((leto, po_letih[leto][1]))
    return po_letih2

Za drugi del bi uporabil še bližnjico, ki je še ne znamo in tako bi dobili zgledno kratek program.

def najboljsi_po_letih(vrstice):
    po_letih = {}
    for vrstica in vrstice:
        ime, leto, cas1, cas2 = podatki(vrstica)
        if not leto in po_letih or (cas2, ime) < po_letih[leto]:
            po_letih[leto] = cas2, ime
    return [(leto, ime) for leto, (cas, ime) in sorted(po_letih.items())]

Raztezne vaje (Zelo dodatna naloga)

Napiši funkcijo preberi_podatke(url), ki takšne podatke prebere direktno s spleta. Lahko se omejiš na 10 km tek, lahko pa poskrbiš tudi za druge razdalje. Podatki so na strani http://vw-ljubljanskimaraton.si/sl/result/20lm.

Ta naloga je precej lažja od prejšnjih zelo dodatnih nalog. Odkriti moraš, kako s Pythonom brati spletne strani (kar ni big deal), potem pa malo uporabljati split in rezine.

Rešitev

Naloga ni bila jasno podana. Namerno: zelo dodatne naloge so pač za tiste, ki bi radi še malo raziskovali. No, tule je program, s katerim sem pobral s spleta podatke, ki ste jih dobili v datoteki 10z.txt.

import urllib.request

g = open("10z.txt", "w")
s = urllib.request.urlopen("http://www.timingljubljana.si/lm/10M.asp").read().decode("cp1250")
vrstice = s.split("<TR")[2:]
for vrstica in vrstice:
    podatki = [x[:-5] for x in vrstica.split("<td>")[1:]]
    podatki[-1] = podatki[-1][:7]
    if not podatki[-1].startswith("DNF"):
        g.write("\t".join(podatki) + "\n")

Tole pa je še nekaj boljšega: pobere podatke za vse dolžine in jih zloži v datoteko s smiselno postavljenimi stolpci, ki jo lahko odprete in se potem igrate z njo tudi v Excelu.

import urllib.request

totcols = []
runners = []
for d in (10, 21, 42):
    for spol in "mz":
        f = urllib.request.urlopen("http://www.timingljubljana.si/lm/{}{}.asp".format(d, spol))
        f = f.read().decode("cp1250")
        cols = f.readline().strip().split("\t")
        fixed = {"Spol": spol.replace("z", "f"), "Distance": str(d)}
        lines = f.readlines()
        for line in lines:
            data = dict(zip(cols, line.strip().split("\t")))
            data["Place_rel"] = str(float(data["Uvr"]) / len(lines))
            data.update(fixed)
            runners.append(data)

cols = [("Year", "LR"), ("Gender", "Spol"), ("Distance", "Distance"),
        ("Country", "Država"), ("Result", "Rezultat"),
        ("Place", "Uvr"), ("Place_rel", "Place_rel"),
        ("5 km", "5 km"), ("10 km", "10 km"), ("15 km", "15 km"),
        ("20 km", "20 km"), ("25 km", "25 km"), ("30 km", "30 km"),
        ("35 km", "35 km"), ("40 km", "40 km")]

g = open("results.tab", "wt")
g.write("\t".join(c[0] for c in cols) + "\n")
cols = [c[1] for c in cols]
for runner in runners:
    if not cols[-1].startswith("DNF"):
        g.write("\t".join(runner.get(col, "") for col in cols) + "\n")
g.close()
Last modified: Tuesday, 23 March 2021, 8:33 PM