Danes se bomo igrali z želvo. Iz dveh razlogov. Želva je zanimiva žival, ki je zelo primerna tudi za otroke. Seymour Papert, eden od tesnejših sodelavcev pomembnega razvojnega psihologa Piageta, je naredil za poučevanje programiranja na konstruktivističen (ali kakršenkoli že) način, jezik Logo, ki se ves vrti okrog želve. Logo je sicer nekoliko iz mode (zaradi nekaterih lastnosti je kot jezik za otroke po mojem skromnem mnenju nekoliko neroden), želve pa so še vedno popularne.

Prvi motiv za današnje predavanje je torej ta, da je to, kar se boste naučili, morda nekoč v neki obliki primerno za poučevanje otrok. Tudi moj starejši sin ima rad Logo in njegovo želvo.

Drugi motiv, zaradi katerega to temo predavam tudi na FRI, pa je, da lahko želve služijo kot odličen uvod v objektno programiranje. Danes bomo spoznali le živalco, torej "podatkovni tip" Turtle, naslednji teden pa bomo podatkovni tip (takrat mu bomo rekli "razred") tudi sami sprogramirali.

Nekaj o jeziku programov: doslej smo programirali tako, da smo imena spremenljivk in funkcij običajno pisali v slovenščini. Dasiravno je to, kljub svojih arhaizmom in celo prav zavoljo njih, moj najljubši jezik, imam z njim probleme, čim programi postanejo malo daljši. Še vedno, kadar sem se lotil pisati slovenske spremenljivke, se je končalo s čubodro, že zato, ker ob svoji kodi vedno uporabljamo tudi tujo in še svoje stare knjižnice, ki smo jih iz takšnih in drugačnih razlogov morali pisati v angleščini. Tudi pri tem predmetu bomo poslej vedno pogosteje posegli po angleških imenih.

Želva

Podatkovni tipi, ki smo jih videvali doslej so bili int, ki je predstavljal cela števila, float, ki je predstavljal necela števila, list, ki je predstavljal sezname, set, ki je predstavljal množice ... in tako naprej. Danes bomo spoznali podatkovni tip Turtle, ki bo predstavljal želve.

Že doslej smo objekte (besedo objekt smo prvič zavestno uporabili, ko smo se začeli pogovarjati o imenih, posebej od naslednjega tedna pa bo končno izrekana v svojem pravem pomenu) včasih naredili tako, da smo povedali njihovo ime. Tako smo se navadili, da z int naredimo število iz niza. Množico smo včasih definirali tako, da smo našteli njene elemente ({1, 2, 3}) prazno množico pa smo dobili tako, da smo rekli set() - "poklicali" smo podatkovni tip.

Z želvami bo podobno: objekt vrste Turtle naredimo tako, da rečemo, recimo

t = Turtle()

S t = Turtle() smo skonstruirali novo želvo in jo priredili spremenljivki t.

Želva lahko naredi podano število korakov naprej ali nazaj, tako da pokličemo njeni metodi forward(s) (v našem primeru, ko imamo želvo t, bomo rekli, recimo, t.forward(10) in backward(s); x je število korakov v točkah (pikslih).

Zna se tudi obračati; pokličemo lahko turn(phi), kjer je phi kot v stopinjah, pri čemer so pozitivni koti v smeri urinega kazalca (da ne bo preveč preprosto!). Poleg ima tudi metodi left() in right(), ki obrneta želvo za 90 stopinj v levo in desno. V začetku je želva na sredi okna in gleda navzgor. Če jo želimo premakniti in preobrniti, pokličemo fly(x, y, phi); ta želvo odnese na postavljene koordinate in jo obrne v želeno smer. Kot 0 stopinj kaže navzgor.

Želva ima tudi pero, ki je lahko spuščeno ali dvignjeno, tako da želva vleče (ali pa ne) za seboj črto. V začetku je spuščeno; dvignemo ga s pen_up() in spustimo s pen_down().

Pa še par nerisarskih zadev. Želvi lahko rečemo, malo počaka, tako da pokličemo metodo wait(t). Kot argument povemo čas čakanja v sekundah. Lahko pa ji naročimo, naj počaka po vsakem risarskem ukazu, tako da pokličemo metodo set_pause(t). Pri tem t spet pove čas v sekundah. Če se čakanja naveličamo, pokličemo no_pause().

Želva je na sliki vidna. Če jo želimo skriti, pokličemo hide(), s show() pa jo spet prikličemo.

Tule je še enkrat ves seznam:

  • forward(s), backward(s): pojdi s točk naprej oz. nazaj;
  • turn(phi) obrni se za phi stopinj v smeri urinega kazalca;
  • left(), right(): obrni se za 90 stopinj levo oz. desno;
  • fly(x, y, phi): "poleti" na koordinati x, y in se obrni v smer phi;
  • pen_up(), pen_down(): dvigni oz. spusti pero;
  • wait(t) počakaj t sekund;
  • set_pause(t), no_pause(): nastavi oz. prekliči čakanje po vsakem ukazu;
  • hide(), show(): pokaži oz. skrij želvo.

Kljub svoji pritlehnosti je želva imenitna žival. Narisati zna, recimo, kvadrat, recimo s stranico 100, tako da gre štirikrat naprej za 100 točk in se nato obrne levo:

for i in range(4):
    t.forward(100)
    t.left()

Veste, kaj? Tole spravimo kar v funkcijo.

def square(turtle, l):
    for i in range(4):
        turtle.forward(l)
        turtle.left()

Funkcija pričakuje dva argumenta, želvo, s katero naj riše, in dolžino stranice. Naprej pa gre tako, kot prej.

Pri funkciji square je posebej zanimivo in dobrodošlo, da je želva po risanju kvadrata obrnjena natanko tako, kot je bila pred njim. To nam omogoča takšnole igro: narišemo kvadrat, nato nekoliko zasukamo želvo, narišemo nov kvadrat, spet zasukamo in to ponavljamo toliko časa, dokler ne pridemo naokrog. Tule je 5 kvadratov, zasukanih za 72 stopinj.

for i in range(5):
    square(turtle, 150)
    turtle.turn(72)

In 90 kvadratov zasukanih za 4 stopinje.

for i in range(90):
    square(t, 150)
    t.turn(4)

Ob funkciji za kvadrat se hitro domislimo, kako risati mnogokotnike. Namesto štirih bomo naredili k korakov, v vsakem koraku bomo nagnali želvo za določeno razdaljo naprej in jo obrnili za ... koliko? Za koliko stopinj se moramo obrniti, da narišemo šestkotnik? Nobene posebne geometrije ne potrebujemo, če se spomnimo, da moramo biti na koncu obrnjeni tja, kot smo bili v začetku. Poln kot ima 360 stopinj, za šestkotnik se bomo v vsakem od šestih korakov obrnili za 60 stopinj. Za k-kotnik pa za 360/k.

def polygon(turtle, a, k):
    for i in range(k):
        turtle.forward(a)
        turtle.turn(360 / k)</xmp>

S funkcijo brez znoja narišemo sedemkotnik, le polygon(t, 50, 7) pokličemo. Tudi risanje snežink je z želvo trivialno: poženemo jo za določeno razdaljo naprej, ji rečemo, naj pride nazaj; to ponovimo k-krat, vmes pa jo obračamo za kot 360 / k.

def flake(turtle, a, k):
    for i in range(k):
        turtle.forward(a)
        turtle.backward(a)
        turtle.turn(360 / k)

Funkciji za risanje mnogokotnika in snežinke sta si simpatično podobni. Razlika je le v tem, da enkrat začnemo v vogalu mnogokotnika, drugič v središču snežinke. Polmer snežinke je enak podani dolžini a, pri mnogokotniku pa je a dolžina stranice.

Ob snežinki navrzimo še funkcijo, ki izriše lepšo snežinko, tako ki je definirana tako, da iz vsakega kraka gledata še po dva kraka, pod kotoma 30 stopinje levo in desno ter z dolžino f-krat (npr. 2-krat ali 1.4-krat krajšo) od kraka, iz katerega izvirata. Podkraka pa imata svoja podpodkraka, ti imajo podpodpodkrake in tako naprej, dokler njihove dolžine niso krajše od 5 točk.

def krak(turtle, a, f=2):
    turtle.forward(a)
    if a > 5:
        turtle.turn(30)
        krak(turtle, a / f, f)
        turtle.turn(-60)
        krak(turtle, a / f, f)
        turtle.turn(30)
    turtle.backward(a)

En krak, f = 1.4:

f = 2:

f = 1.4:

Najlepše snežinke pa nam z želvo skuha Koch.

def brokenLine(turtle, a):
    if length < 5:
        turtle.forward(a)
    else:
        brokenLine(turtle, a / 3)
        turtle.turn(-60)
        brokenLine(turtle, a / 3)
        turtle.turn(120)
        brokenLine(turtle, a / 3)
        turtle.turn(-60)
        brokenLine(turtle, a / 3)

t = Turtle()
t.fly(100, 120, 90)

for i in range(3):
    brokenLine(t, 400)
    t.turn(120)</xmp>

Cvetje

Dovolj snega, čas je za pomlad.

Kaj dobimo, če narišemo 100-kotnik, s polygon(turtle, 5, 100)? Stokotnik je pravzaprav že krog... S kakšnim polmerom? No, to je pa težje reči. Dalo bi se izračunati... Vendar pustimo: namesto tega, kakšen je polmer, se raje vprašajmo, kakšen je obseg. No, to je preprosto: če gre želva stokrat naprej za 5 točk, je obseg 500. Ha! Če vemo, kakšen je obseg kroga, pa menda vemo tudi njegov polmer, ne? Polmer kroga z obsegom 500 je 500/ 2 * pi.

Zdaj lahko obrnemo: napisali bomo funkcijo krog(turtle, r), ki nariše krog s polmerom r. Namesto 100-kotnika bomo risali kar 360 kotnik (to nam bo prišlo nekoliko prav kasneje, boste videli). Obseg kroga je 2 * pi * r; če ga hočemo izrisati v 360 korakih, bomo v vsakem koraku prehodili 2 * pi * r / 360. Formula mi je tako všeč, da jo bom kar obrnil: r * 2 * pi / 360. Kaj je 2 * pi / 360? Toliko radianov je ena stopinja. Kaj pa imajo zdaj s tem radiani in stopinje? Imajo; vendar nam morda potegne malo kasneje. Zdaj pa le brž narišimo krog.

from math import pi

def krog(t, r):
    for i in range(360):
        t.forward(r * 2 * pi / 360)
        t.turn(1)

Naredimo torej 360 korakov, v vsakem prehodimo tristošestdesetinko obsega in se obrnemo za eno stopinjo, kar je tristošestdesetinka polnega kota.

Bi znali narisati tudi lok? Del kroga? Znamo napisati funkcijo lok(turtle, r, kot)? Ker smo se, zvito, odločili risati 360-kotnike, bomo lok narisali kot del 360-kotnika -- če hočemo lok s kotom 50 stopinj, bomo narisali le prvih 50 oglišč 360-kotnika.

def lok(t, r, kot):
    for i in range(kot):
        t.forward(r * 2 * pi / 360)
        t.turn(1)

Zdaj lahko pomislimo, kaj imajo s tem radiani. Če računamo kote v radianih, kako dolg je krožni lok s kotom fi, če ima krog polmer r? Če je fi v radianih, je to kar r * fi. Dolžina našega loka je r * kot * 2 * pi / 360 (ker smo kot-krat prehodili r * 2 * pi / 360). Kot je podan v stopinjah, kot * 2 * pi / 360 pa je ta kot v radianih. Pa smo pri r * fi.

Zdaj, ko znamo risati loke, se naučimo risati cvetne liste. Cvetni list bo sestavljen iz dveh 60-stopinjskih lokov. Vedno bomo risali cvetne liste v smeri, v katero je obrnjena želva. Predstavljajmo si, da je obrnjena navzgor. Če pokličemo lok(t, r, 60), bomo dobili lok, ki bo oklepal os 60 stopinj z vodoravnico (od navpičnice je torej nagnjen za 30 stopinj) in želva bo po koncu risanja obrnjena za 60 stopinj od navpičnice (očitno, saj se bo 60-krat obrnila za eno stopinjo). Če hočemo, da bo list navpičen, bomo želvo najprej obrnili za 30 stopinj v levo in šele nato narisali 60-stopinjski lok. Želva bo po tem obrnjena za 30 stopinj desno od navpičnice (ker bi bila brez obračanja za 60, bo zaradi začetnega obračanja za 30 v levo, obrnjena le še za 30 v desno). Lok nazaj dol bi si želeli risati pod kotom 30 stopinj glede navpičnico - navzdol, torej 150 stopinj glede na navpičnico. Če smo trenutno obrnjeni za 30 stopinj, se je potrebno obrniti še za 120. Po tem narišemo lok in na koncu je želva obrnjena ... Uh, temu je nekoliko težko slediti, zato bo najboljše, če si narišete in razmislite. Izkaže se, da je potrebno želvo na koncu zasukati še za 150 stopinj, da bo po risanju lista ponovno obrnjena navzgor.

def list(t, r):
    t.turn(-30)
    lok(t, r, 60)
    t.turn(120)
    lok(t, r, 60)
    t.turn(150)

Ako hočemo narisati cvet s šestimi cvetnimi listi, pač šestkrat pokličemo funkcijo list in se vmes vsakokrat zasučemo za 60 stopinj. Če bi hoteli risati osemlistne rože, osemkrat pokličemo list, vmes pa se vrtimo za 360/8 stopinj. Ali, v splošnem:

def roza(t, r, l=6):
    for i in range(l):
        list(t, r)
        t.turn(360/l)

Zvezda

Narišimo petkrako zvezdo. Ob pripravi na predavanja sem stvar (najbrž?) narobe razmislil in sklepal takole: v petkotniku se v vsakem oglišču obrnem za 72 stopinj. Pri risanju zvezde hočem vedno preskakovati po eno vozlišče, torej se bom obračal za 144 stopinj.

def zvezda(t, r):
    for i in range(5):
        t.forward(r)
        t.turn(144)

In tako dobim zvezdo

Ob pisanju zapiskov sem reč ponovno razmislil, pošteno preračunal in skiciral. Rezultat je pravilen; tudi, če bi na podoben način razmišljali o sedemkraki zvezdi, bi z gornjim razmislekom o "preskakovanju oglišč" dobili pravilen kot (le da bi preskočili drugo število oglišč, koti v ogliščih so drugačni in zato je drugačen tudi kot, za katerega se mora želva obrniti). Premislek kot tak pa, čeprav vedno da pravilen rezultat, ni pravilen - oziroma, točneje, ne vidim, zakaj bi bil pravilen. Zanimivo.

Spirala

Preprosto kvadratno spiralo lahko narišem tako, da se lotim risati kvadrat (v tem smislu, da se na vsakem vogalu obrnem za 90 stopinj), vendar ne rišem le štirih stranih, poleg tega pa stalno povečujem dolžino stranice. Takole:

def spirala(t):
    a = 1
    for i in range(100):
        t.forward(a)
        a += 3
        t.right()

Last modified: Monday, 18 April 2016, 10:30 PM