Testi

Testi: testi-potniki-2.py

Naloga

Podan je razred Potnik.

class Potnik: def __init__(self, energija): self.x = self.y = 0 self.__energija = energija def pojdi(self, smer, razdalja): x0, y0 = self.x, self.y self.premik(smer, razdalja) self.__energija += 5 * (x0 * self.x < 0) + (y0 * self.y < 0) def porabi(self, energija): if self.__energija < energija: return False self.__energija -= energija return True

Potniki začenjajo svojo pot na koordinatah (0, 0) in imajo določeno količino energije. Nato hodijo naokrog tako, da kličemo metodo pojdi, ki ji kot argument podamo smer in razdaljo; na kakšen način ju podajamo, boste videli spodaj. Vsaka prehojena enota razdalje potniku vzame eno enoto energije; vsakič ko prečka os x ali os y, dobi 5 enot energije. (Koti in koordinate so obrnjene tako kot v matematiki - pi/2 kaže navzgor, to je, v smeri osi y).

Metoda pojdi deluje tako, da pokliče metodo premik(smer, razdalja). Ta še ni napisana - napisati jo boste morali sami. Poleg tega pojdi preveri, ali je potnik ob tem premiku prečkal os x ali y in poveča energijo.

Metoda porabi preveri, ali ima potnik še dovolj energije, da prehodi določeno razdaljo. Če je nima, vrne False; če jo ima, vrne True in zmanjša potnikovo energijo. Potnik mora imeti pred odhodom na pot dovolj energije za celo pot; če bi na poti slučajno dobil energijo, ker bo prestopil os, to ne šteje.

Razreda Potnik ne smete spreminjati.

Obvezna naloga

Iz razreda Potnik boste izpeljali tri razrede. Vsi trije bodo imeli (poleg podedovanih) le eno metodo, namreč premik. Metoda premik mora primerno klicati Potnikovo metodo porabi.

Vaša metoda premik mora sprejeti argumente v primerni obliki, izračunati razdaljo, ki naj bi jo potnik prehodil, s klicem metod porabi odšteti energijo in, če porabi ne vrne False (kar bi pomenilo, da potnik nima dovolj energije), potnika dejansko prestaviti na nove koordinate, tako da spremeni self.x in self.y.

Vaša metoda premik naj ne uporablja (direktno) atributa __energija (dva podčrtaja na začetku sta dogovorjeni znak za "pusti pri miru!"), temveč naj z energijo dela le prek metode porabi.

Orto se premika le orgotonalno, se pravi, na jug, sever, vzhod in zahod. Njegova metoda premik(smer, razdalja) naj pričakuje, da bo smer eden izmed nizov "S", "J", "V" ali "Z", razdalja pa pač razdalja, ki naj jo prehodi v tej smeri.

OrtoPlus pozna poleg tega še smeri "SV", "SZ", "JV" in "JZ". Pomen argumenta razdalja je takšen: če pokličemo pojdi("SV", 1), bo šel potnik za eno enoto na sever in za eno na vzhod - potuje naravnost, torej po diagonali. Skupaj bo torej prepotoval (približno) 1.41 enote, torej ga bo to stalo tudi toliko energije.

Liberalec dobi smer podano kot kot (v radianih), razdalja pa je razdalja, ki jo bo naredil v tej smeri.

Rešitev

Vsi trije izpeljani razredi bodo torej imeli metodo premik, ki pokliče podedovano porabi in če ta vrne True, spremeni self.x in self.y. Metode se razlikujejo le po tem, za koliko premaknejo in s kakšnim argumentom kličejo porabi.

class Orto(Potnik): def premik(self, smer, razdalja): if not self.porabi(razdalja): return dx, dy = {"S": (0, 1), "J": (0, -1), "V": (1, 0), "Z": (-1, 0)}[smer] self.x += razdalja * dx self.y += razdalja * dy class OrtoPlus(Potnik): def premik(self, smer, razdalja): dx, dy = {"S": (0, 1), "J": (0, -1), "V": (1, 0), "Z": (-1, 0), "SV": (1, 1), "SZ": (-1, 1), "JV": (1, -1), "JZ": (1, -1) }[smer] s = razdalja * sqrt(dx ** 2 + dy ** 2) if self.porabi(s): self.x += razdalja * dx self.y += razdalja * dy class Liberalec(Potnik): def premik(self, smer, razdalja): if self.porabi(razdalja): self.x += razdalja * cos(smer) self.y += razdalja * sin(smer)

Pri prvih dveh razredih se je pokazalo, da niste preveč zvesti bralci mojih rešitev domačih nalog. V rešitvi naloge Pike bi med drugim lahko videli tale trik:

smeri = {"L": (-1, 0), "R": (1, 0), "U": (0, 1), "D": (0, -1)} for znak in vpis[2:]: dx, dy = smeri[znak]

To je praktično enako temu, kar zgoraj počneta Orto in OrtoPlus. Podobne reči smo počeli v domačih nalogah tudi v preteklih letih, torej bi lahko isti trik videli tudi tam.

Žal je bila večina rešitev v slogu spodnje rešitve.

def premik(self,smer, razdalja): if len(smer) == 1: poraba = super().porabi(razdalja) else: razdalja_diagonala = float(sqrt((self.x + razdalja )**2 + (self.y + razdalja)**2)) poraba = float(super().porabi(razdalja_diagonala)) if poraba != False and (smer == "S" or smer == "J" or smer == "V" or smer == "Z"): if "S" == smer: self.y = self.y + razdalja elif "J" == smer: self.y = self.y - razdalja elif "V" == smer: self.x = self.x + razdalja elif "Z" == smer: self.x = self.x - razdalja elif poraba != False and (smer == "SV" or smer == "SZ" or smer == "JV" or smer == "JZ"): if smer == "SV": self.y = self.y + razdalja self.x = self.x + razdalja elif smer == "SZ": self.y = self.y + razdalja self.x = self.x - razdalja elif smer == "JV": self.y = self.y - razdalja self.x = self.x + razdalja elif smer == "JZ": self.y = self.y -razdalja self.x = self.x - razdalja

Gre za izdelek enega od študentov, vendar bi si lahko izbral tudi katerega drugega. Tule je slučajno zbrano skupaj veliko reči, ki so vredne komentarja.

Najprej: vidim, da sem vas zmedel s super(). Žal sem ga moral pokazati, ko smo sestavljali nekoliko bolj zapletene konstruktorje. Zapomnite si tole: super uporabimo samo, ko kličemo podedovano metodo, ki smo jo povozili. S super povemo, da ne bi radi poklicali svoje metode, temveč podedovano. Razredi, izpeljani iz razreda Potnik, nimajo svoje metode porabi, zato je self.porabi isto (a običajnejše in jasnejše) kot super().porabi.

Še bolj ne delajte tega:

if Potnik.porabi(self, razdalja):

Kaj naredi tole, se sploh nismo učili. S tem pokličemo metodo porabi, ki pripada razredu, ne objektu, zato moramo v klicu "ročno" dodati self. To v Pythonu 3 zelo redko počnemo (prej je bilo včasih v določenih kontekstih praktično, zdaj pa tak način klicanja potrebujemo le redko.

Vrstica

razdalja_diagonala = float(sqrt((self.x + razdalja )**2 + (self.y + razdalja)**2))

je napačna iz več razlogov. Kot prvo, napačna je formula: če sem na točki (102, 196) in se premaknem za 5 korakov na SZ, bo razdalja, ki jo bom naredil, natančno enaka, kot če bi bil na točki (-21, 42) in se premaknil za pet korakov proti SZ. Torej self.x in self.y sploh nista pomembna.

Program je slučajno prestal teste, ker pač ni bilo testa, ki bi poskušal, kaj se dogaja na kakšnih specifičnih koordinatah. Če bi bilo med testi, recimo, tole, pa bi pokazali napako:

potnik2 = OrtoPlus(10) potnik2.pojdi("S", 8) potnik2.pojdi("SZ", 1) self.assertEqual(potnik2.x, 11)

S prvim korakom pride potnik na koordinate (0, 8) in ima še 2 enoti energije. En korak proti SZ bi mu vzel 1.41 energije; ker je ima dovolj, bi se premaknil na (1, 9). Vendar gornja formula pravi, da je razdalja enaka korenu iz (self.x + 1) ** 2 + (self.y + 1) ** 2; ker je self.y enak 8, je to koren iz 82.

Za začetek moramo torej pobrisati self.x in self.y.

razdalja_diagonala = float(sqrt(razdalja ** 2 + razdalja ** 2))

Za float ni prav nobene potrebe: rezultat sqrt-ja je že float, torej ni potrebe, da bi ga spreminjali v float.

razdalja_diagonala = sqrt(razdalja ** 2 + razdalja ** 2)

Veliko vas je napisalo nekaj takšnega. Tule pa pride prav malenkost matematike. To je isto kot sqrt(2 * razdalja ** 2), kar je isto kot razdalja * sqrt(2). Se pravi

razdalja_diagonala = razdalja * sqrt(2)

Še bolj nerodna je naslednja vrstica, ki pravi

poraba = float(super().porabi(razdalja_diagonala))

namesto

poraba = self.porabi(razdalja_diagonala)

Tu je float celo zelo napačen: rezultat klica porabi je True ali False in nekaj vrstic kasneje program celo preverja if poraba != False - torej očitno predpostavlja, da je še vedno tipa bool.

Večina je v svojih funkcijah napisala nekaj v slogu

if len(smer) == 1: p = razdalja else: p = razdalja * sqrt(2) if self.porabi(p): ... in tako naprej

Nekdo je to genialno rešil takole:

if self.porabi(razdalja * sqrt(len(smer)):

Če je smer samo "S", "J", "V" ali "Z" bo sqrt(len(smer)) enak 1 in kar piše je isto kot self.porabi(razdalja). Če ima smer dve črki, pa množimo s korenom 2. To bi delovalo celo v treh dimenzijah, kjer bi lahko šli še gor in dol ter zato občasno množili s korenom iz 3. Super!

Sledi

if poraba != False and (smer == "S" or smer == "J" or smer == "V" or smer == "Z"):

Če poraba ni False je True. In nekateri so v resnici pisali (manj ponesrečeni) if poraba == True: ali, kot je prišlo izgleda v modo ob tej nalogi, if poraba is True:. Še vedno ne razumem, kaj je narobe iz if poraba:. :)

Ista pesem, le v drugem molu - iz drugega izdelka.

if len(smer) == 2: if self.porabi(sqrt(razdalja*razdalja+razdalja*razdalja)) != True: return False else: if self.porabi(razdalja) != True: return False

x != True je isto kot not x. :)

Izogibajte se tega

if any(smer == direction for direction in ("SZ", "SV", "JZ", "JV")): razdalja *= sqrt(2) if self.porabi(razdalja): (...) elif smer == "SZ": self.x -= razdalja / sqrt2

Lepše je

if any(smer == direction for direction in ("SZ", "SV", "JZ", "JV")): pot = razdalja * sqrt(2) else: pot = razdalja if self.porabi(pot): (...) elif smer == "SZ": self.x -= razdalja

Ideja, da pripravimo spremenljivko z resnično razdaljo, je čisto lepa. Ni pa dobro, da za to žrtvujemo ime razdalja, saj vsebuje pravo razdaljo, ki jo bomo kasneje še potrebovali. V splošnem je to slaba ideja zaradi nepreglednosti, kadar delamo z necelimi števili, pa je ideja še toliko slabša. Zavedati se moramo, da necela števila niso povsem natančna in z vsako operacijo bomo izgubili nekaj natančnosti. Tule se to še ne pozna, v kakšnih resnejših izračunih pa bi nas lahko začelo tepsti.

Naloga je večkrat pokazala na vaše pomanjkljivo znanje matematike. Tule je še en zanimiv primer.

Nekateri ste smeri najprej pretvorili v kote (kar je slaba ideja, ker potem stvari niso več nujno natančne. Nato je iz tega izračunal premike.

if smer == "V": smer = 0 ... in tako naprej phi = radians(90-smer) prilezna = sin(phi) * razdalja neprilezna = cos(phi) * razdalja nx = self.x + prilezna ny = self.y + neprilezna

Kote je bilo potrebno pretvoriti v radiane. Ne bi bilo za inženirja spodobno, da bi kote že zapisal v radianih? Torej, da bi vedel, da je pi/2 90 stopinj in tako naprej?

Še bolj nerodno je, da so ti koti odšteti od 90 stopinj in se vrtijo v napačno smer. To smo imeli pri želvi, ker smo kote zapisovali tako, kot bi bili bližji otrokom (0 kaže gor in potem v smeri urinega kazalca). Tu pa tole le vse pomeša ... kar se vidi po tem, da v programu piše prilezna = sin(phi) * razdalja. Dolžino priležne stranice navadno dobimo s kosinusom, ne sinus. In, ja, "vodoravni" je kosinus. Do te zmede pride zaradi tistega 90 - smer. Programer mora znati matematiko.

V tem pogledu je še bolj simpatično tole: pretvorimo kot iz radianov v stopinje, da jih bomo potem pretvarjali nazaj v radiane.

class Liberalec(Potnik): def premik(self, smer, razdalja): stopinje = math.degrees(smer) if self.porabi(razdalja): if stopinje == 0: self.x += razdalja else: self.x += math.cos(stopinje * math.pi / 180) * razdalja self.y += math.sin(stopinje * math.pi / 180) * razdalja

Mimogrede, zakaj kot 0 stopinj obravnavati ločeno?

Še bolj žalostne reči so se dogajale s kvadranti. Funkciji sinus in kosinus že povesta, kar je treba - kvadrantov ni potrebno obravnavati ločeno. Nekaj od spodnjega je prav, nekaj pa tudi ne (vendar testi niso preverjali ravno vseh možnih smeri).

class Liberalec(Potnik): def premik(self, smer, razdalja): smer = abs(smer) a = math.pi/2 if super().porabi(razdalja): if 0 <= smer < a: self.x, self.y = self.x + razdalja * math.cos(smer), self.y - razdalja * math.sin(smer) elif a <= smer < math.pi: self.x, self.y = self.x - razdalja * math.cos(smer), self.y - razdalja * math.sin(smer) elif math.pi <= smer < 3*a: self.x, self.y = self.x + razdalja * math.cos(smer), self.y + razdalja * math.sin(smer) elif 3*a <= smer < 2*a: self.x, self.y = self.x + razdalja * math.cos(smer), self.y + razdalja * math.sin(smer)

Primerjajte to z rešitvijo, ki jo napišejo takšni, ki poznajo kotne funkcije:

class Liberalec(Potnik): def premik(self, smer, razdalja): if self.porabi(razdalja): self.x += razdalja * cos(smer) self.y += razdalja * sin(smer)

Nekoga bi bilo treba tepsti. :) Predpostavljam, da to, da desetkrat prišteješ 1, namesto da bi enkrat prištel deset, ne odraža neznanja matematike. ;)

def premik(self, smer, razdalja): book_of_wisdom = {'Z': (-1, 0), 'V': (1, 0), 'S': (0, 1), 'J': (0, -1)} soon_x, soon_y = book_of_wisdom[smer] if self.porabi(razdalja): for i in range(razdalja): self.x += soon_x self.y += soon_y

Še en primer predolge rešitve:

class OrtoPlus(Potnik): def premik(self, smer, razdalja): if smer == "S": if not self.porabi(razdalja): return False else: self.y += razdalja if smer == "J": if not self.porabi(razdalja): return False else: self.y -= razdalja if smer == "V": if not self.porabi(razdalja): return False else: self.x += razdalja if smer == "Z": if not self.porabi(razdalja): return False else: self.x -= razdalja if smer == "SV": if not self.porabi(sqrt(2*(razdalja**2))): return False else: self.x += razdalja self.y += razdalja if smer == "JV": if not self.porabi(sqrt(2*(razdalja**2))): return False else: self.x += razdalja self.y -= razdalja if smer == "SZ": if not self.porabi(sqrt(2*(razdalja**2))): return False else: self.x -= razdalja self.y += razdalja if smer == "JZ": if not self.porabi(sqrt(2*(razdalja**2))): return False else: self.x -= razdalja self.y -= razdalja

Ko programirate, vedno razmišljajte, kaj storiti, da vam ne bi bilo potrebno ponavljati kode. Tule se štirikrat ponovi

if not self.porabi(razdalja): return False else:

in še štirikrat

if not self.porabi(sqrt(2*(razdalja**2))): return False else:

Oboje se da narediti enkrat za štirikrat, kot v prejšnjih rešitvah.

Dodatna naloga

Razrede v obvezni nalogi smo zastavili nekoliko nerodno. Kot ste najbrž opazili, ste v vseh metodah premik pisali podobne stvari. Tule je boljši osnovni razred; poimenovali smo ga Potnik2. Napiši razrede Orto2, OrtoPlus2, Liberalec2, ki so izpeljani iz razreda Potnik2, vedejo pa se enako kot njihovi soimenjaki brez dvojk.

Kako deluje lepše zasnovani Potnik2 in kaj mora početi njihova metoda premik, pa razberi sam(a) - tudi to je del naloge.

class Potnik2: def __init__(self, energija): self.x = self.y = 0 self.__energija = energija def pojdi(self, smer, razdalja): dx, dy = self.premik(smer, razdalja) if not self.porabi(sqrt(dx ** 2 + dy ** 2)): return x0, y0 = self.x, self.y self.x += dx self.y += dy self.__energija += 5 * (x0 * self.x < 0) + (y0 * self.y < 0) def porabi(self, energija): if self.__energija < energija: return False self.__energija -= energija return True

Rešitev

Kot je lepo zapisal nek študent:

# Razred Potnik2 je boljši, ker od metode premik(...) zahteva le # koordinati x in y. Le-ti nista atributa objekta, temveč navadni # spremenljivki, kateri vrnemo, funkcija pojdi(...) pa nato opravi # vse nadaljne "vpisovanje" novih vrednosti v atribute objekta.

Z drugimi besedami: vse tisto, kar je skupno vsem izpeljanim razredom, naj bo raje v osnovnem razredu, Potnik. Ko si izmišljamo hierarhijo razredov in njihove metode, jih postavimo tako, da bo vsa skupna koda zbrana na enem mestu, v predniku, ne pa v vsaki metodi posebej. Če moramo v vseh izpeljanih razredih preverjati, ali imamo dovolj energije, je to naloga za osnovni razred.

Metoda premik v dodatni nalogi mora vrniti premik v smeri x in y. To, kako iz podane razdalje in smeri določiti premik, je namreč edino, po čemer se razredi razlikujejo, torej naj bo to edino, kar je prepuščeno specifičnim metodam.

Sprogramirati je bilo potrebno le tole:

class Orto2(Potnik2): def premik(self, smer, razdalja): return {"S": (0, razdalja), "J": (0, -razdalja), "V": (razdalja, 0), "Z": (-razdalja, 0)}[smer] class OrtoPlus2(Potnik2): def premik(self, smer, razdalja): return {"S": (0, razdalja), "J": (0, -razdalja), "V": (razdalja, 0), "Z": (-razdalja, 0), "SV": (razdalja, razdalja), "SZ": (-razdalja, razdalja), "JV": (razdalja, -razdalja), "JZ": (razdalja, -razdalja) }[smer] class Liberalec2(Potnik2): def premik(self, smer, razdalja): return razdalja * cos(smer), razdalja * sin(smer)
Zadnja sprememba: četrtek, 25. marec 2021, 21.33