Maraton
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 return
u 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()