Problem: če prižigam in ugašam luč, magnetni senzor zaznava obrate, ko kolo v hrčkovi kletki v resnici miruje.

Rešitev: naj ti študenti sprogramirajo ustrezni filter.

Predpostavili bomo, da merimo čase v sekundah (ni čisto res) in da lahko to stvar programiramo v Pythonu (tudi ni čisto res).

Testi

testi-hrcek-na-kolesu.py

Obvezna naloga

Napisati bo potrebno naslednji kup funkcij.

  • v_seznam(s) prejme čase, ko gre magnet mimo senzorja, v obliki niza, v katerem so števila zapisana z decimalno vejico in med seboj ločena s podpičji. Funkcija naj vrne seznam števil. Tako mora klic v_seznam("5,180; 5,907; 6,632; 7,215") vrniti [5.180, 5.907, 6.632, 7.215].

  • v_niz(s) naredi ravno obratno. (Ne ukvarjaj se s številom decimalnih mest. Pusti, naj Python naredi, kar naredi.)

  • oznaci_veljavne(s) prejme seznam časov. Vrne enako dolg seznam vrednosti True in False, pri čemer je False na mestih, kjer so neveljavne meritve. Neveljavne so tiste meritve, ki so od kake druge meritve oddaljene manj kot 0.1 sekunde, saj hrček ne more narediti kroga tako hitro. Klic oznaci_veljavne([5.1, 5.6, 6.0, 10.34, 10.37, 10.45, 12.5]) mora vrniti [True, True, True, False, False, False, True], saj so prve tri meritve veljavne (razmik med njimi je večji od 0.1), naslednje tri neveljavne, zadnja pa je spet veljavna.

  • veljavne(s) prejme seznam meritev in vrne seznam veljavnih meritev. Klic veljavne([5.1, 5.6, 6.0, 10.34, 10.37, 10.45, 12.5]) vrne [5.1, 5.6, 6.0, 12.5].

  • Recimo, da včasih dobimo čase v napačnem vrstnem redu. Predpostavimo, da gre za neko napako v sistemu in magnet v tistem času ni prečkal senzorja. Napišite funkcijo brez_napacnih_casov(s), ki prejme seznam časov in vrne seznam, v katerem ni takšnih časov. Klic brez_napacnih_casov([5, 20, 10, 15, 30]) mora vrniti [5, 20, 30]. Časa 10 in 15 preskoči, ker sta se pojavila po času 20.

Rešitev

(Še kar) lepa rešitev bi bila tale - ki sicer žal ne deluje čisto, ampak jo bomo že še popravili.

def v_seznam(s):
    casi = []
    for cas in s.split(";"):
        cas = cas.replace(",", ".")
        cas = float(cas)
        casi.append(cas)
    return casi

Če funkcija vrača seznam, moramo pač narediti (prazen) seznam. Niz s razbijemo glede na podpičja (s.split(";")). Tako dobimo seznam nizov, na primer ["5,180", "5,907", "6,632", "7,215"] Ne da bi to kam shranjevali, se kar takoj zapodimo čezenj z zanko. V vsakem nizu zamenjamo nesrečno decimalno vejico s piko, cas = cas.replace(",", "."). Nato ga spremenimo v število, cas = float(cas). To dodamo v seznam, pa je.

(Upam, da cenite mojo potrpežljivost. :) Normalno bi človek to naredil v enem zamahu, casi.append(float(cas.replace(",", "."))). Tule pa gremo lepo počasi in pregledno.)

Tole ne deluje, če je niz s prazen. V tem primer s.split(";") vrne seznam z enim samim praznim nizom, [""]. Zanka for se bo tako izvedla, cas bo enak "" in Python se bo v klicu float(cas) pritožil, da praznega niza ne more spremeniti v število.

Na začetku funkcije zato ločeno poskrbimo za prazen niz, recimo tako.

def v_seznam(s):
    if not s:
        return []
    casi = []
    for cas in s.split(";"):
        cas = cas.replace(",", ".")
        cas = float(cas)
        casi.append(cas)
    return casi

Mimogrede, tole je malo poseben primer. Navadno se nam s praznimi seznami ne bo potrebno posebej ukvarjati; lepo napisane funkcije bodo kar same od sebe delovale tudi zanje.

Nazaj se vrnemo z obratnimi koraki v obratnem vrstnem redu.

def v_niz(casi):
    s = []
    for cas in casi:
        cas = str(cas)
        cas = cas.replace(".", ",")
        s.append(cas)
    return "; ".join(s)

Že naslednji teden se bomo naučili, da je tidve funkciji možno napisati veliko krajše.

def v_seznam(s):
    if not s:
        return []
    return [float(x.replace(",", ".")) for x in s.split(";")]


def v_niz(casi):
    return "; ".join(str(x).replace(".", ",") for x in casi)

Funkcija oznaci_veljavne je lahko prav zoprna. Ni pa nujno. Tule je kar lepa rešitev.

def oznaci_veljavne(casi):
    veljavne = [True] * len(casi)
    for i in range(len(casi) - 1):
        if casi[i + 1] - casi[i] < 0.1:
            veljavne[i + 1] = veljavne[i] = False
    return veljavne

Najprej pripravimo ustrezno dolg seznam True-jev. Potem se zapeljemo prek podanih časov. Primerjamo pare zaporednih elementov (casi[i+ 1] in casi[i]). Če sta preblizu skupaj, oba razveljavimo.

Grešil sem proti lastnemu priporočilu in uporabil range(len(...)). Prav, pa gremo še brez njega.

def oznaci_veljavne(s):
    veljavne = [True] * len(casi)
    for i, e in enumerate(casi[:-1]):
        if casi[i + 1] - e < 0.1:
            veljavne[i + 1] = veljavne[i] = False
    return veljavne

Kaj pa vem... Tole je kvečjemu manj pregledno od prve rešitve. Pravilo tule pač nima smisla: potrebujemo ta element in naslednji element in indeks tega in indeks naslednjega ... Ja, lahko zipamo in enumeriramo, ampak ... tule pravilo pač ne deluje dobro.

Zdaj pa še seznam veljavnih meritev. Ker smo si pripravili funkcijo oznaci_veljavne, to ni preveč težko.

def veljavne(casi):
    oznake = oznaci_veljavne(casi)
    veljajo = []
    for cas, veljaven in zip(casi, oznake):
        if veljaven:
            veljajo.append(cas)
    return veljajo

Sestavimo seznam oznak. Nato gremo z zip-om vzporedno čez oba seznama, čez čase in oznake. Čas dodamo, če je veljaven.

Že naslednji teden se bomo naučili hitrejše poti.

def veljavne(s):
    return [x for x, y in zip(s, oznaci_veljavne(s)) if y]

Funkcija, ki odstrani prevelike čase, si mora le zapomniti največji čas, ki ga je videla doslej. Vsak novi čas doda le, če je večji od največjega doslej.

def brez_napacnih_casov(casi):
    pravilni = []
    naj = -1
    for cas in casi:
        if cas > naj:
            pravilni.append(cas)
            naj = cas
    return pravilni

Eh, kako brez potrebe kompliciramo. Največji čas je pač zadnji čas s seznama, ne?

def brez_napacnih_casov(casi):
    pravilni = []
    for cas in casi:
        if cas > pravilni[-1]:
            pravilni.append(cas)
    return pravilni

Da, vendar ne čisto. V začetku je seznam prazen, zato bo ob pravilni[-1] Python zastokal Index out of range. Nič hudega: pač začnimo tako, da bo brez_napacnih_casov v začetku že vseboval prvi čas. Če je koga strah, da ga bo v zanki še enkrat dodal: ne bo, saj ne bo večji od zadnjega časa v zanki. :)

def brez_napacnih_casov(casi):
    pravilni = [casi[0]]
    for cas in casi:
        if cas > pravilni[-1]:
            pravilni.append(cas)
    return pravilni

Vendar to še vedno ne deluje. Kaj, če je seznam casi prazen? V tem primeru bomo dobili Index out of range že zaradi casi[0]. No, takole bo pa šlo.

def brez_napacnih_casov(casi):
    pravilni = casi[:1]
    for cas in casi:
        if cas > pravilni[-1]:
            pravilni.append(cas)
    return pravilni

Rezine delujejo tudi, če so indeksi preveliki. Če seznam ne vsebuje niti enega elementa, bo casi[:1] vrnil prazen seznam.

Dodatna naloga

Napiši še en kup funkcij. :)

  • odstrani_neveljavne(s), ki je podobna funkciji veljavne. Razlika je v tem, da funkcija odstrani_neveljavne ne sme vrniti ničesar, temveč pobriše elemente iz seznama, ki ga je dobila kot argument.

  • najv_hitrost(s, o) vrne največjo hitrost, ki jo je dosegel hrček. Argument o je obseg kolesa. Pri računanju ne upoštevaj lažnih meritev, med katerimi je manj kot desetinka sekunde. Prav tako ne upoštevaj posamičnih obratov, to je, obratov, ki so od najbližjega (veljavnega) obrata oddaljeni več kot dve sekundi (saj hrček ne more vrteti tako počasi :). Klic najv_hitrost([24, 60, 205.134, 205.182, 205.190, 205.207, 512.73, 513.20, 513.65], 45) vrne 100. 24 in 60 sta neveljavni, ker sta osamljeni, one okrog 205 pa so preblizu skupaj. Najmanjša razlika med veljavnima je 513.65 - 513.20, to je 0.45. Če naredi 45 cm v 0.45 sekunde, teče 100 cm na sekundo. Če hitrosti ni mogoče izračunati, ker ne obstajajo zaporedne uporabne meritve, naj funkcija ne vrne ničesar (t.j., vrne None).

  • na_kolesu(s) pove, koliko časa je hrček gonil kolo. Funkcija vrne vsoto razlik med pari časov, ki so veljavni in niso osamljeni (po definiciji iz prejšnje naloge).

Rešitev

V funkciji odstrani_neveljavne je bilo potrebno razumeti predvsem, kako spremenimo seznam, ki smo ga dobili kot argument.

def odstrani_neveljavne(casi):
    casi = veljavne(casi)

ne deluje. S tem samo imenu casi priredimo nek nov seznam, namesto da bi spreminjali tisti seznam, ki smo ga dobili kot argument.

Kot sem pokazal na predavanjih, vsebino seznama najlažje spremenimo tako, da priredimo vrednost rezini, ki predstavlja celotni seznam.

def odstrani_neveljavne(casi):
    casi[:] = veljavne(casi)

Sicer pa bo tole tema za eno celo predavanje čez par tednov.

Zdaj pa največja hitrost.

def najv_hitrost(s, o):
    s = veljavne(s)
    najm = 2
    for e, f in zip(s, s[1:]):
        if f - e < najm:
            najm = f - e
    if najm < 2:
        return o / najm

Najprej se znebimo neveljavnih meritev. Potem pa moramo le še poiskati najmanjšo razliko med paroma. Za začetek bomo predpostavili, da je enaka 2; zaradi omejitev naloge bo morala biti namreč manjša, ali pa bo funkcija na koncu vrnila None. Z zip gremo - kot sem pokazal na začetku prejšnjih predavanj! - čez zaporedne pare. Če je razlika manjša od najmanjše doslej (in s tem seveda tudi manjša od 2), si jo zapomnimo. Na koncu vrnemo hitrost, a le, če smo naleteli na kako razliko manjšo od 2. Sicer pa nič.

def na_kolesu(s):
    s = veljavne(s)
    v = 0
    for e, f in zip(s, s[1:]):
        if f - e < 2:
            v += f - e
    return v

Merjenje časa, preživetega na kolesu, je skoraj ista reč.

def na_kolesu(s):
    s = veljavne(s)
    v = 0
    for e, f in zip(s, s[1:]):
        if f - e < 2:
            v += f - e
    return v

Obe funkciji sta lahko tudi bistveno krajši.

def najv_hitrost(s, o):
    s = veljavne(s)
    najm = min((f - e for e, f in zip(s, s[1:]) if f - e < 2), default=0)
    if najm > 0:
        return o / najm

def na_kolesu(s):
    s = veljavne(s)
    return sum((f - e for e, f in zip(s, s[1:]) if f - e < 2), 0)
Last modified: Tuesday, 23 March 2021, 8:33 PM