Testi

Testi: testi-pike.py

Naloga

Pri opisu rešitve te naloge bom izpostavil nekaj študentov, a brez imen. Če bo med prikazanimi izdelki tudi vaš, se ne čutite se nagovorjene. Če ste ga bom pokazal kot primer nečesa, kar se ne dela: drži, vendar ste se do tega prebili v potu svojih prstov in se naučili veliko več kot nekdo, ki je oddal perfektno rešitev - ki ni njegova. Če bom pokazal vaš izdelek kot primer, kako se dela, v resnici pa ga sploh niste naredili sami: niste nategnili mene, temveč sebe. Domače naloge so namenjene vam, ne meni; jaz bi že znal tole sprogramirati (če ne, bi pa prosil Jureta. ;) Seveda pa ne bom omenjal imen, saj niso pomembna.

Naloga je bila posrečena, saj ste se uspešno spotikali ob kupe nastavljenih pasti. Mislim, da je bilo kar poučno - za tiste, ki ste se bili voljni učiti.


Programirali smo Dots. Če je ne boste preskusili sami (ne priporočam, včasih malo zasvoji), si jo oglejte, recimo, na You Tubu.

Različne barve so predstavljene s številkami od 1 do 5. Stanje igralne plošče je shranjeno v seznamu seznamov. Imamo torej spremenljivko (navadno jo bomo imenovali polje), ki ima toliko elementov, kolikor je stolpcev, in vsak element bo vseboval toliko števil, kolikor je vrstic. Elemente stolpcev pišemo od spodaj navzgor. Če bi torej radi tretji element druge vrstice (od spodaj), bomo rekli polje[3][2] (pri čemer številčenje seveda začnemo z 0, kot vedno).

Za razliko od prave igre, v kateri je polje vedno dimenzij 6x6, je naše polje lahko poljubno veliko ali pa celo nekoliko dolgočasno; imamo lahko tudi le eno samo vrstico ali en sam stolpec.

V nekaterih funkcijah bomo govorili tudi o poti: pot je seznam koordinat, na primer [(3, 1), (3, 2), (4, 2), (4, 3)]. Koordinate, ki jih vsebuje imajo lahko poljuben vrstni red.

Ogrevanje

Napiši funkcijo dopolni(polje, visina). Argument polje je seznam seznamov števil. Funkcija mora dopolniti vse (pod)sezname tako, da bodo imeli visina elementov. Dopolniti ga mora z naključnimi števili med 1 in 5.

Funkcija ne vrača rezultata, temveč spreminja poslano polje.

Če imamo, recimo, polje = [[2, 4], [], [1, 2, 3]], mora dopolni(polje, 3) dodati en element v prvi podseznam, tri v drugega in nobenega v tretjega.

Rešitev

Tole je bila le kar preprosta vaja iz zank in seznamov - pa razmišljanja o tem, kako jih uporabiti seveda.

Gremo po stolpcih (for stolpec in polje) in v vsakega dodajamo elemente (stolpec.append(randint(1, 5))), dokler jih nima toliko, kolikor jih mora imeti (while len(stolpec) < visina).

def dopolni(polje, visina): for stolpec in polje: while len(stolpec) < visina: stolpec.append(randint(1, 5))

Za oceno 6: Izpis

Napiši funkcijo izpisi(polje), ki vrne niz z vsebino polja - če bi ta niz izpisali, bi dobili polje, kot ga vidi igralec. Upoštevati mora torej, da seznam polje vsebuje stolpcev, zapisane od spodaj navzgor. Tako mora

polje = [[1, 2, 4, 4], [4, 1, 4, 2], [2, 1, 4, 3], [2, 4, 2, 3], [1, 2, 3, 4]]

vrniti niz

42334 44423 21142 14221

Rešitev

Zanimivo, koliko vetra vam je dala ta funkcija. Eden od študentov jo je rešil takole:

def izpisi(polje): tmp = "" for i in range(len(polje[0]), 0, -1): for j in range(len(polje)): tmp += str(polje[j][i - 1]) tmp += "\n" return tmp

Le malo jo olepšajmo: namesto i in j pišimo x in y, da bomo vedeli, kaj je kaj. Pa meje prve zanke spremenimo, da bo y tekel prek pravih številk in mu ne bo potrebno stalno odštevati enice.

def izpisi(polje): tmp = "" for y in range(len(polje[0]) - 1, -1, -1): for x in range(len(polje)): tmp += str(polje[x][y]) tmp += "\n" return tmp

To je bila nekako pričakovana rešitev (žal večina sicer ni bila tako elegantna, nekatere pa so bile tudi preveč elegantne v slabem pomenu besede).

Takole: v niz tmp dodajamo polja, tmp += str(polje[x][y]). Poskrbeti moramo, da bosta zanki, ki določata x in y tekli prek primernih koordinat.

Ker izpisujemo po vrsticah, bo šla zunanja zanka prek y. Ker so stolpci shranjeni tako, da je tisto, kar je najvišje (in je torej potrebno izpisati najprej), na koncu, bo tekla v nasprotno smer. Zato for y in range(len(polje[0]) - 1, -1, -1). Notranja zanka, x, gre lepo od leve proti desni, for x in range(len(polje)). Z tmp += "\n" na koncu vsake vrstice dodamo znak za novo vrstico.

Če pogledamo malo natančneje, opazimo, da gre notranja zanka prek stolpcev. Torej jo naženimo prek stolpcev, namesto da se hecamo z nekimi xi.

def izpisi(polje): tmp = "" for y in range(len(polje[0]) - 1, -1, -1): for stolpec in polje: tmp += str(stolpec[y]) tmp += "\n" return tmp

Sam sem v svoji rešitvi to (zmerno) stisnil tako, da sem zamenjal notranjo zanko z join in generatorjem.

def izpisi(polje): izpis = "" for vrstica in range(len(polje[0]) - 1, -1, -1): izpis += ("".join(str(stolpec[vrstica]) for stolpec in polje)) + "\n" return izpis

Tole je še na meji prebavljivo preglednega. Tole pa je brez zveze, saj je takole mogoče le pisati, brati pa pravzaprav ne.

def izpisi(polje): return "\n".join(("".join(str(stolpec[vrstica]) for stolpec in polje)) for vrstica in range(len(polje[0]) - 1, -1, -1))

Nekateri ste ob izpisovanju spreminjali polje. Ko sem zagrozil, da tega ne počnite, ste se rešili tako, da ste ga skopirali. Vendar tole ne deluje, kot si je avtor zamislil:

def izpisi(polje): igrisce = str() polje2 = polje.copy() while len(polje2[0]) != 0: for vrsta in polje2: igrisce = igrisce + str(vrsta[-1]) del vrsta[-1] igrisce = igrisce + '\n' return igrisce

Teste je prestalo zgolj zato, ker nisem preverjal, ali pustite polje pri miru (ker pač nisem pomislil na to, da bi ga kdo hotel pri izpisu spreminjati. Zakaj, kako? Mar polje.copy() ne naredi kopije? Da, naredi: dobimo novo polje, vendar vsebuje iste elemente, torej iste podsezname. Če jih spreminjate, spreminjate podsezname originalnega polja. Še nekaj o tem bomo videli pri naslednji nalogi, ki je bila bolj namenjena razumevanju teh reči.

Za oceno 7: Brisanje

Napiši funkcijo pobrisi_pot(pot, polje), ki pobriše pike na poljih, ki se nahajajo na poti. Pike nad njimi naj "padejo" en element nižje. Nekateri seznami bodo zaradi tega torej nekoliko krajši.

Poleg tega napiši funkcijo pobrisi_barvo(barva, polje), ki odstrani vse pike določene barve.

Funkciji ne vračata ničesar, temveč spreminjata polje, ki sta ga dobili kot argument.

Rešitev

Na tole nalogo sem pa nekoliko ponosen. Učila vas je razlike med del in remove, učila vas je, kako spreminjati seznam, ki ga dobi funkcija (in kako se ga ne da spreminjati), poleg tega pa vas je učila, kako izvajati zaporedna brisanja v seznamu, pri katerem se vam elementi "izpodmikajo". Predvsem slednje vas je urilo tudi v iskanju napak v svojem programu.

Brisanje barve - in kako spreminjati polje

Najprej napišimo drugo, manj poučno funkcijo.

def pobrisi_barvo(barva, polje): for stolpec in polje: while barva in stolpec: stolpec.remove(barva)

Problem, ki ste ga imeli nekateri, je, da ste izpustili while - očitno v veri, da bo remove odstranil vse pojavitve barve. Ne, odstrani le prvo, zato jo je potrebno odstranjevati, dokler je popolnoma ne izkoreninimo.

Variacija tega je

def pobrisi_barvo(barva, polje): for stolpec in polje: while stolpec.count(barva) > 0: stolpec.remove(barva)

To je sicer podobno, vendar manj učinkovito: veliko hitreje je preveriti, ali stolpec vsebuje nekaj, kot šteti, kolikokrat vsebuje. V tej nalogi se to seveda prav nič ne pozna. Vedite pa za sicer.

Nekateri ste raje uporabljali izpeljane sezname. Če bi me kdo vprašal, ali ni to lepše, mu ne bi rekel, da ni. Vodi pa v poučen problem. Spodnja rešitev namreč ne deluje.

def pobrisi_barvo(barva, polje): novo = [] for stolpec in polje: novo.append([x for x in stolpec if x != barva]) polje = novo

Problem je v tem, da funkcija ne spremeni vrednosti polja, ki ga je dobila kot argument, temveč le priredi imenu polje nek nov seznam.

Očitno boljša ideja je, da spraznimo seznam polje in dodajamo vrstice vanj. Predtem pa seveda shranimo staro vsebino polja v novo spremenljivko.

def pobrisi_barvo(barva, polje): staro = polje polje.clear() for stolpec in staro: polje.append([x for x in stolpec if x != barva])

Ideja je lepa, problem pa je v tem, da sta staro in polje le dve imeni za en in isti seznam.

Oboje - varianta "staro" in varianta "novo" - je mogoče poflikati. Varianto "novo" popravimo tako, da na koncu funkcije zamenjamo vsebino seznama polje z novo vsebino:

def pobrisi_barvo(barva, polje): novo = [] for stolpec in polje: novo.append([x for x in stolpec if x != barva]) polje[:] = novo

S tem zamenjamo trenutno vsebino polja ("od začetka do konca") z neko drugo vsebino.

Krajša različica slednjega, ki jo je napisal nekdo, je

def pobrisi_barvo(barva, polje): polje[:] = [[tocka for tocka in podseznam if tocka is not barva] for podseznam in polje]

Korektna rešitev z izpeljanim seznamom.

Kdor teh reči ne razume, naj nujno prebere zapiske predavanj o imenskih prostorih.

Flikanje variante "staro" je ravno nasprotno (in zato skoraj enako). Imenu staro priredimo nov seznam, ki vsebuje iste reči kot polje.

def pobrisi_barvo(barva, polje): staro = polje[:] polje.clear() for stolpec in staro: polje.append([x for x in stolpec if x != barva])

Nekateri ste uporabili deepcopy.

def pobrisi_barvo(barva, polje): staro = copy.deepcopy(polje) polje.clear() for stolpec in staro: polje.append([x for x in stolpec if x != barva])

Če napišemo staro = polje[:], bo staro seznam, ki vsebuje iste elemente kot polje - torej iste sezname. Funkcija deepcopy pa skopira v globino - ne naredi le kopije polja, temveč tudi kopije njegovih elementov in tako naprej. Seznam staro bo po tem vseboval sezname (stolpce), ki bodo enaki kot seznami, ki jih vsebuje polje, ne bodo pa isti.

Kaj je pravilno? [:] je enostavnejše; če je tudi dovolj, je boljše, saj ni potrebe po tem, da bi kopirali v globino. No, izkaže se, da je dovolj: polje.clear() sicer izprazni seznam, tako da vrže iz njega vse njegove elemente - elementom, torej stolpcem, pa ne naredi hudega, torej jih ni potrebno kopirati.

Še ena simpatična rešitev, ki jo je napisal nek študent, je

def pobrisi_barvo(barva, polje): for i, x in enumerate(polje): polje[i] = [y for y in x if y != barva]

Zakaj to deluje? Ker prireja nove sezname seznamu polje - torej prav tistemu seznamu, ki ga je potrebno spreminjati. Kdor ne razume, zakaj polje = ... ni dobra ideja, polje[i] = ... pa je, naj si ogleda zapiske prej omenjenih predavanj in razmisli, kaj se dogaja v ozadju.

Brisanje poti - in težave z izpodmikanjem

Tule vas je čakala prava skala spotike. Oglejmo si jo na nekoliko preprostejšem primeru.

>>> a = [0, 1, 2, 3, 4, 5] >>> p = [0, 1, 2] >>> for e in p: del a[e]

Če pobrišemo ničti, prvi in drugi element, bi človek pričakoval, da bodo v seznamu ostali 3, 4 in 5. Ker nesreča ne počiva in ima hudič prste vmes in kar je še takšnega v ljudskem izročilu, pa je a takšen:

>>> a >>> [1, 3, 5]

Zakaj? Preprosto: ko pobrišemo ničti element, postane enica ničti element in dvojka prvi. Ko pobrišemo prvi element, pobrišemo dvojko (ne enice, kot smo hoteli). Zdaj se vse premakne še za eno mesto; no, ne vse - enka ostane, kjer je bila, vsekakor pa na drugo mesto pride štirica. Tako takrat, ko pobrišemo drugi element, pobrišemo štirico namesto dvojke.

Funkcija

def pobrisi_pot(pot, polje): for x, y in pot: del polje[x][y]

za katero so mnogi za sveto trdili, da je gotovo pravilna, ne deluje iz istega razloga.

Napaka je bila za večino tako nepričakovana, da ste se ob njej urili v tem, da ste raziskovali, kaj neki je narobe v vašem programu. Koristna vaja; upam, da ste jo čimbolj vadili sami.

Zdaj pa rešitve. Oglejmo si tri možnosti.

Prvi je bil med vašimi rešitvami najpopularnejši: vse, kar je potrebno pobrisati, zamenjamo z -1, None, "x" ali čim podobno nemogočim. Šele, ko je vse označeno, pobrišemo vse -1, None in "x".

Tole je eden od študentskih izdelkov.

def pobrisi_pot(pot, polje): for x, y in pot: polje[x][y] = None for i in polje: while None in i: i.remove(None)

V drugi zanki razpoznamo isto reč kot brisanje barve. Nekateri so se dejansko znašli in pisali

def pobrisi_pot(pot, polje): for x, y in pot: polje[x][y] = None pobrisi_barvo(polje, None)

Lepo!

Med pregledovanjem nalog sem naleten na še eno odlično različico istega. Tole mi ni prišlo na misel. Če se je avtor tega spomnil sam: bravo!

def pobrisi_pot(pot, polje): for x, y in pot: polje[x][y] = 999 for i in range(len(polje)): polje[i] = list(filter((999).__ne__, polje[i]))

Tole je žlahtna kombinacija objektnega in funkcijskega programiranja: oni čudni stavek pravi "izberi vse tiste, za katere 999 pravi, da jim ni enaka.

Drugi obvoz je enako očiten, vendar zahteva - če nočemo, da postane zapleten, nekaj programerske spretnosti.

def pobrisi_pot(pot, polje): def izpodmik(pot, x, y): return sum(y0 < y for x0, y0 in pot if x0 == x) for i, (x, y) in enumerate(pot): del polje[x][y - izpodmik(pot[:i], x, y)]

Zanka for i, (x, y) in enumerate(pot) pomeni približno isto kot for x, y in pot, le da zraven še šteje, kolikšen del poti smo že prehodili. Nato brišemo, vendar namesto del polje[x][y] pišemo del polje[x][y - izpodmik(pot[:i], x, y)], pri čemer nam bo funkcija

izpodmik(pot[:i], x, y) izračunala, za koliko je dosedanja pot (pot[:i]) izpodmaknila element, ki bi moral biti na x, y (v resnici bi morali pisati pot[:i - 1], vendar bi to povzročilo težave pri i=0, to, da smo v resnici vzeli en element preveč, pa bo minilo brez posledic).

Kaj mora narediti izpodmik? No, povedati, koliko elementov stolpca xz indeksom manjšim od y so že pobrisali. Kaj počne y0 < y for x0, y0 in pot if x0 == x? Vzame vse koordinate na dosedanji poti; označimo jih z x0 in y0. Zanimajo nas samo takšne, ki so v pravem stolpcu, torej x == x0. Pri vseh takšnih preverimo, ali velja y0 < y. Rezultat tega je True ali False. Če vemo, da je True isto kot 1 in False isto kot 0, bo vsota teh reči ravno število elementov, ki so bili v istem stolpcu in je njihova koordinata y0 manjša od podanega y.

Kot rečeno, ni zapletno, le nekaj spretnosti zahteva.

Tretji obvoz je eleganten. Če bi morali pobrisati prvi, drugi in tretji element, bi imeli težavo, če tretjega, drugega in prvega, pa bi bilo vse v redu. Ker pa gre načelno za eno in isto, se lahko znajdemo tako, da pot uredimo v padajočem vrstnem redu, torej reversed(sorted(pot)) ali, učinkoviteje, sorted(pot, reverse=True). Rešitev naloge je torej kar

def pobrisi_pot(pot, polje): for x, y in sorted(pot, reverse=True): del polje[x][y]

Pri tem se nam ni potrebno ukvarjati s tem, ali bomo urejali po x ali po y. Kot smo se že parkrat omenili, Python primerja terke "leksikografsko": najprej primerja prve elemente, če so ti enaki, druge in tako naprej. Tu bo torej uredil padajoče po koordinatah x (stolpci), znotraj stolpcev pa padajoče po koordinatah y. To je za naše potrebe čisto v redu.

V oči mi je padlo tole:

def pobrisi_pot(pot, polje): #reversed(sorted.. mi ni treba se ukvarjat s slabim indeksiranjem v iteracijah for tocka in reversed(sorted(pot)): stolpec = tocka[0] pika = tocka[1] del polje[stolpec][pika]

Zanimivo se mi zdi, da je tu nekdo odkril najkrajšo rešitev, obenem pa brez potrebe pisal

for tocka in reversed(sorted(pot)): stolpec = tocka[0] pika = tocka[1]

namesto jedrnatega Pythonovskega

for stolpec, pika in reversed(sorted(pot)):

Občutek imam, da je še precej takšnih, ki sicer znate programirati že od prej, ni pa vam do tega, da bi se naučili programirati v Pythonu. Kot moj brat, ki je med potovanjem po Češkem natepaval dunajske zrezke namesto (odličnih, to je bilo pred petindvajsetimi leti, pred komercializacijo in množičnim turizmom) knedličke z golažem.

Tole razpakiranje ni izključno Pythonovska ideja; prišel je iz funkcijskih jezikov, kjer je to kar običajna reč, prej ko slej pa ga bodo vključili tudi v Javascript, v katerem boste gotovo nekoč programirali (Firefox in Opera to že podpirata). (Sicer pa je tudi Javascript funkcijskih jezik; zadnjič sem bral nekoga, ki pravi, da je to Lisp s C-jevsko sintakso.)

del in remove

V funkciji pobrisi_barvo je bilo potrebno pobrisati element z znano vrednostjo, za katerega nismo vedeli, kje je. To se dela z remove. Seveda bi ga lahko poiskali sami (ali pa uporabili index) in ga pobrisali z del, torej nekako tako: del stolpec[stolpec.index[barva]]. Vendar je stolpec.remove(barva), ki naredi natančno isto, očitno preprostejši.

V funkciji pobrisi_pot natančno vemo, kje je reč, ki jo hočemo brisati, ne vemo pa, kakšna je. No, pravzaprav vemo: če hočemo pobrisati polje[x][y], potem vemo, da je njegova vsebina pač vrednost polje[x][y]. Vendar je polje[x].remove(polje[x][y]) zelo slaba ideja, saj pobriše prvo (torej najnižjo) pojavitev vrednosti polje[x][y] v stolpcu polje[x]. Če slučajno hočemo pobrisati drugo ... bo narobe.

Nekdo je odlično sprogramiral vso domačo nalogo, vključno s to funkcijo, vendar je na koncu uporabil remove namesto del:

def pobrisi_pot(pot, polje): for del_poti in sorted(pot, reverse=True): polje[del_poti[0]].remove((polje[del_poti[0]][del_poti[1]])) return polje

Ta funkcija na deluje, če v (dolgočasnem, eno-stolpčnem) polju [[1, 2, 3, 2]] poskusimo pobrisati pot [(0, 3)], to je, drugo dvojko. Metoda remove bo pobrisala prvo dvojko, na katero bo naletela in rezultat bo [[1, 3, 2]] namesto [[1, 2, 3]].

Nekateri namesto del uporabljate pop. S tem ni nič narobe, vsa razlika med del s[i] in s.pop(i) je v tem, da pop vrne rezultat, namreč pobrisani element, del pa ne. (No, še ena je: pop je izraz in ga lahko uporabimo na kakem mestu, na katerem dela ne moremo.) Vseeno priporočam del, zaradi berljivosti: ko vidim del, vem, da brišemo. Ko vidim pop si predstavljam, da se bo s tem elementom nekaj zgodilo in moram še enkrat pogledati, da vidim, da rezultata niste nikjer uporabili.

Za oceno 8: Preverjanje poti

Napiši funkcijo preveri_pot(pot, polje), ki pove, ali je pot pravilna in vrne True ali False.

Pot je pravilna, če velja naslednje:

  • Pot je dolga vsaj dve točki.
  • V vsakem koraku gre pot za eno polje gor, dol, levo ali desno.
  • Nobeno polje se ne ponovi; izjema so sklenjene poti, v katerih je prvo polje enako zadnjemu. To je dovoljeno.
  • Celotna pot se nahaja znotraj polja - nobena koordinata ni levo ali desno od polje oziroma nad ali pod njim.
  • Celotna pot teče prek točk iste barve.

Rešitev

Pri reševanju te naloge ste bili res ustvarjalni. Morda je bolj kot druge pokazala, koliko rutinske spretnosti premorete. Nekateri ste jo kar dobro zapletli, drugi ste bili kar učinkoviti.

Storiti nam je tole: zapisati moramo gornje pogoje. Čim kateri od njih ne drži, bomo vrnili False. Pojdimo lepo po vrsti:

  • Pot je dolga vsaj dve točki: tu ni kaj, len(pot) >= 2

  • Nobeno polje se ne ponovi; izjema so sklenjene poti, v katerih je prvo polje enako zadnjemu. Spomnimo se, kako preverjamo, ali vsebuje nek seznam samo unikatne elemente: spremenimo ga v množico in preverimo, ali je ta enako dolga kot seznam, len(pot) == len(set(pot)). To skoraj deluje, razen takrat, ko je zadnji element enak prvemu. V tem primeru bo len(set(pot)) za 1 premajhen. No, pa prištejmo na desni strani še pot[0] == pot[-1]. Če sta enaka, bo to True, True pa je (kar se tiče aritmetika) isto kot 1. Se pravi, len(pot) == len(set(pot)) + (pot[0] == pot[-1]). Ne pozabimo na oklepaje: če bi pisali brez, len(pot) == len(set(pot)) + pot[0] == pot[-1], bi bilo to isto kot len(pot) == (len(set(pot)) + pot[0]) == pot[-1], saj ima seštevanje prednost pred dvojnim enačajem. (Program pa bi javil napako, saj ne more sešteti števila len(set(pot)) in terke pot[0].)

  • Celotna pot se nahaja znotraj polja - nobena koordinata ni levo ali desno od polje oziroma nad ali pod njim. Če znamo uporabiti all, je to preprosto: za vsak par x, y iz poti (for x, y in pot, mora veljati, da sta znotraj polja: all(0 <= x < len(polje) and 0 <= y < len(polje[0] for x, y in pot).

  • V vsakem koraku gre pot za eno polje gor, dol, levo ali desno: šli bomo prek vseh zaporednih koordinat - tega ne delamo prvič, zato hitro rečemo for (x1, y1), (x2, y2) in zip(pot, pot[1:]). Kaj mora veljati za par koordinat? Izračunamo razliko po koordinati x in razliko po koordinati y, torej abs(x1 - x2) in abs(y1 - y2). Eden (in natančno eden) od njiju se mora spremeniti za 1 in drugi za 0. Oba sta pozitivna. Torej le izračunamo vsoto in ta mora biti 1, abs(x1 - x2) + abs(y1 - y2) == 1. To mora veljati za vse pare, se pravi: all(abs(x1 - x2) + abs(y1 - y2) == 1 for (x1, y1), (x2, y2) in zip(pot, pot[1:]))

  • Celotna pot teče prek točk iste barve: tole je nekoliko podobno kot prej, le preprosteje: vsako polje mora biti enake barve kot polje pred njim, all(polje[x1][y1] == polje[x2][y2] in zip(pot, pot[1:])).

Zdaj le zložimo vse pogoje v funkcijo:

def preveri_pot(pot, polje): return \ len(pot) >= 2 and \ len(pot) == len(set(pot)) + (pot[0] == pot[-1]) and \ all(0 <= x < len(polje) and 0 <= y < len(polje[0]) for x, y in pot) and \ all(abs(x0 - x1) + abs(y0 - y1) == 1 for ((x0, y0), (x1, y1)) in zip(pot, pot[1:])) and \ all(polje[x0][y0] == polje[x1][y1] for ((x0, y0), (x1, y1)) in zip(pot, pot[1:]))

Opazimo še, da imamo v zadnjih dveh enako zanko, torej ju lahko združimo v

def preveri_pot(pot, polje): return \ len(pot) >= 2 and \ len(pot) == len(set(pot)) + (pot[0] == pot[-1]) and \ all(0 <= x < len(polje) and 0 <= y < len(polje[0]) for x, y in pot) and \ all(abs(x0 - x1) + abs(y0 - y1) == 1 and polje[x0][y0] == polje[x1][y1] for ((x0, y0), (x1, y1)) in zip(pot, pot[1:]))

Če niste tako okretni z izpeljanimi seznami, je lahko rešitev seveda nekoliko daljša. Ne bomo jih kazali, saj jih je precej in vsaka je zavozlana na svoj način. Nekateri ste se sicer lepo organizirali in za vsako pogoj napisali ločeno funkcijo. To je lepa navada. Tudi v resničnem življenju ne gredo vsi pogoji v eno vrstico, zato je lepo, da se jih naučite razbiti na funkcije.

Če počnete to, vedite, da lahko v Pythonu (in drugih sodobnih jezikih) pišemo funkcije tudi znotraj funkcij. Navadite se tega sloga.

Tule je še primer, kako ne programirajte.

def preveri_pot(pot, polje): p=True t=pot[0] u=pot.copy() del(u[0]) lent=0 w=len(polje) f=len(polje[0]) s=[] b=poisci_barvo(pot,polje) if b!=[]: c=b[0] h=pot.copy() las=pot[-1] del(h[-1]) if len(pot)<2: p=False for i,j in u: p=False if i-1==t[0] or i+1==t[0] or i==t[0]: if j-1==t[1] or j+1==t[1] or j==t[1]: p=True if i!=t[0] and j!=t[1]: p=False t=(i,j) for i,j in u: if i>=(w-1) or j>(f-1)or j<0 or i<0: p=False if pot[0]!=pot[-1]: for i in pot: for j in s: if i==j: p=False break s.append(i) else: for i in h: for j in s: if i==j: p=False break s.append(i) for i in b: if i!=c: p=False t=pot[0] for i,j in u: if (t[0]-i)>1 or (t[1]-j)>1 or (i-t[0])>1 or(j-t[1])>1: p=False break t=(i,j) return p

Tega ne objavljam, da bi se posmehovali: avtor te rešitve se je res potrudil. Bolj kot mnogi drugi. In program je po svoje v redu ... le očitno kaže, da bi ga bilo smiselno razdeliti na funkcije. Pa še nekaj: uporabljajte zgovornejša imena spremenljivk. V takšni kodi se je nemogoče znajti...

Za oceno 9: Pot iz koordinat

Napiši funkcijo pot_iz_koordinat(opis), ki dobi niz, ki opisuje pot in ga spremeni v seznam koordinat. Niz se začne z dvema števkama, ki predstavljata koordinati x in y, sledi pa jima zaporedje premikov levo, desno, gor in dol, opisanih s črkami L, R, U in D ;(angleško, ker se v slovenščini desno in dol začneta z isto črko).

Niz "35LLUR" pomeni, da začnemo na (3, 5) in nadaljujemo levo, levo, gor in desno, torej mora pot_iz_koordinat("35LLUL") vrniti seznam [(3, 5), (2, 5), (1, 5), (1, 6), (2, 6)].

Rešitev

Tole je tehnikalija: kako spreminjati L, R, U in D v premike si lahko ogledate v sto in eni varianti lanskih nalog s čebelo, v starih izpitih se pojavljajo tudi neki pajki... Tule je ena od preprostejših različic.

def pot_iz_koordinat(vpis): smeri = {"L": (-1, 0), "R": (1, 0), "U": (0, 1), "D": (0, -1)} x, y = int(vpis[0]), int(vpis[1]) pot = [(x, y)] for znak in vpis[2:]: dx, dy = smeri[znak] x += dx y += dy pot.append((x, y)) return pot

Za oceno 10: Igra

Najprej sprogramiraj funkcijo ni_sosednjih(polje), ki vrne True, kadar na plošči ni več sosednjih točk iste barve. Vsaka točka ima štiri sosede (ali manj, na robu in v vogalih); diagonalne sosede ne štejejo.

Nato sestavi celotno igro - v tekstovnem načinu. Igra naj teče na plošči velikosti 6x6, ki jo obvezno napolni takole

polje = [[] for i in range(6)] dopolni(polje, 6)

Nato naj ponavlja naslednje: izpiše trenutni položaj, uporabnik vnese pot (kot je opisana v nalogi za oceno 9), program preveri, ali je pot veljavna in če ni, izpiše "Neveljavna pot" (in ponovi izpis polja), sicer pa pobriše vse pike na poti, obstoječe pike padejo v nastali prosto, stolpce pa dopolni z novimi naključno izbranimi pikami. Če je pot, ki jo je vnesel uporabnik, sklenjena, pa s plošče pobriše vse točke tiste barve.

Igre je konec, ko na plošči ni več nobenih sosednjih točk iste barve.

Rešitev

Preverjanje sosednosti gre takole: najprej razmislimo, kako bi za en sam stolpec ugotovili, ali ima kje dve sosednji enaki polji - in v tem primeru vrnemo False.

for e, f in zip(stolpec, stolpec[1:]): if e == f: return False

To poženemo seveda čez vse stolpce.

for stolpec in polje: for e, f in zip(stolpec, stolpec[1:]): if e == f: return False

Potem ugotovimo, kako za dva stolpca ugotoviti, ali se v kaki vrstici ujemata.

for e, f in zip(s1, s2): if e == f: return False

To bomo pognali prek vseh sosednjih stolpcev.

for s1, s2 in zip(polje, polje[1:]): for e, f in zip(s1, s2): if e == f: return False

Celotna funkcija je torej

def ni_sosednjih(polje): for stolpec in polje: for e, f in zip(stolpec, stolpec[1:]): if e == f: return False for s1, s2 in zip(polje, polje[1:]): for e, f in zip(s1, s2): if e == f: return False return True

Gre tudi bolj jedrnato.

def ni_sosednjih(polje): return all(e != f for stolpec in polje for e, f in zip(stolpec, stolpec[1:])) and \ all(e != f for s1, s2 in zip(polje, polje[1:]) for e, f in zip(s1, s2))

Zdaj pa še celotna igra.

def igra(): polje = [[] for i in range(6)] dopolni(polje, 6) while not ni_sosednjih(polje): izpisi(polje) vnos = input("Poteza") pot = pot_iz_koordinat(vnos) if not preveri_pot(pot): print("Neveljavna poteza") continue if pot[0] == pot[-1]: x, y = pot[0] pobrisi_barvo(polje[x][y], polje) else: pobrisi_pot(pot, polje)
Zadnja sprememba: četrtek, 25. marec 2021, 21.03