Zapiski
Trop želv
Najprej naredimo nepomembno vajo, ki nam bo pomagala razmišljati. Sestavimo ne eno, temveč pet želv in jih v začetku obrnimo v naključne smeri. Nato stokrat naključno izberimo eno od želv, jo obrnimo za naključen kot med -30 in +30 stopinj ter pošljimo naprej za naključno število od 10 do 20 korakov.
import random
turtles = []
for i in range(5):
t = Turtle()
t.turn(360 * random.random())
turtles.append(t)
for i in range(100):
t = random.choice(turtles)
t.turn(random.randint(-30, 30))
t.forward(random.randint(10, 20)
t.wait(0.5)
Nič takega, nič posebno lepega nismo narisali. Namen vaje je le, da si pravilno predstavljate, da je želv lahko tudi več.
Razred Turtle
Začnimo takole: katere podatke mora shranjevati (vsaka) želva, da lahko deluje? Vedeti mora
- kje je; to bomo shranili v
xiny, - kam je obrnjena: to bomo shranili v
angle, - ali je pero spuščeno ali dvignjeno; to bomo shranili v
pen_active, ki bo imela vrednostTruealiFalse
Najprej sprejmimo tale dogovor: spremenljivki, ki je vsebovala želvo smo doslej
rekli t, kadar je šlo za argument funkcije, pa smo jo imenovali turtle.
Poslej ji bomo iz razlogov, ki bodo kmalu jasni, namesto t ali turtle
rekli self. Želva, self bo torej vsebovala svoje koordinate, kot in stanje
peresa. Vse to bo shranjeno v self.x, self.y, self.angle in
self.pen_active; tem rečem bomo rekli atributi razreda Turtle.
Kako bi bila videti funkcija, ki nastavi pravilne začetne vrednosti vseh teh
atributov? Imenujmo jo - spet iz razlogov, ki bodo jasni čez nekaj vrstic -
__init__. Takšna je.
def __init__(self):
self.x, self.y = risar.maxX / 2, risar.maxY / 2
self.angle = 0
self.pen_active = True
Nič posebnega ne počne. Kot argument dobi želvo self in ji postavi self.x
in self.y na sredo, obrne jo v smeri 0 (self.angle = 0) in spusti pero.
Opogumljeni s preprostostjo te naloge napišimo še funkcijo forward. Ta bo
prejela dva argumenta, želvo (self) in razdaljo, ki naj jo želva prehodi
(a).
Kot pretvorimo v radiane. Nato v nx in ny izračunamo, kam je potrebno
prestaviti želvo. Za malo znanja trigonometrije odkrijemo, da se moramo v smeri
x se premakniti za a * cos(phi) v y pa za `a * sin(phi). Upoštevati moramo
še, da računalnikove koordinate tečejo v napačno smer: če želimo gor, moramo
odštevati, ne prištevati.
Ko je matematika za nami, je vse preprosto: če je pero spuščeno, narišemo črto, v vsakem primeru, ne glede na pero. Pa prestavimo želvo v nove koordinate.
def forward(self, a):
phi = radians(self.angle)
nx, ny = self.x + a * cos(phi), self.y - a * sin(phi)
if self.pen_active:
risar.crta(self.x, self.y, nx, ny)
self.x, self.y = nx, ny
Napišimo še eno funkcijo: obračanje želve. Ta je trivialna in nevredna komentarja.
def turn(self, angle):
self.angle += angle
Skoraj smo že tam, le še zadnji problem rešimo: rekli smo, da bomo napisali
razred Turtle in to, kar smo napisali zdaj, ne bodo funkcije kar tako,
temveč metode tega razreda. Ne želimo jih klicati z, recimo
forward(t, 20), temveč s t.forward(20). Tole pa se naredi takole: zložimo
jih v razred.
from math import *
import risar
class Turtle:
def __init__(self):
self.x, self.y = risar.maxX / 2, risar.maxY / 2
self.angle = 0
self.pen_active = True
def forward(self, a):
phi = radians(self.angle)
nx, ny = self.x + a * cos(phi), self.y - a * sin(phi)
if self.pen_active:
risar.crta(self.x, self.y, nx, ny)
self.x, self.y = nx, ny
def turn(self, angle):
self.angle += angle
S class Turtle: smo napovedali, da sledi definicija razreda. Dvopičju
sledi, kot običajno, zamik. Vse, kar je zamaknjeno, so metode razreda. Bi
lahko bilo preprosteje?
Zdaj povejmo, kakor smo obljubili, še čemu ravno imeni self in __init__.
Prvo pravzaprav ni potrebno. Pisati bi smeli tudi
def turn(zelva, angle):
zelva.angle += angle</xmp>
par vrstic višje pa, prav tako brez zadržkov
def __init__(ta):
ta.x, ta.y = risar.maxX / 2, risar.maxY / 2
ta.angle = 0
ta.pen_active = True
Vendar konvencija pravi, da kot ime, ki ga metode uporabljajo za objekt, vedno
uporabljamo ime self.
Z __init__ pa je drugače. Ko bomo naredili nov objekt, recimo tako, da bomo
poklicali t = Turtle(), bo Python preveril, ali ima razred Turtle metodo
z imenom __init__ in jo poklical. Tu glede izbire imena torej nimamo
svobode. Metodi __init__ pravimo konstruktor.
Napisani razred že ima vse metode, ki jih potrebuje, z njim lahko z malo iznajdljivosti že rišemo. Kvadrat, recimo, bomo naredili z
t = Turtle()
for i in range(4):
t.forward(100)
t.turn(90)
Tako kot prej, torej, le left in right še nimamo, pa se zato znajdemo s
turn.
Mimogrede opazimo nekaj zanimivega: funkcija forward je definirana tako, da
prejme dva argumenta, self in a. Ob klicu smo podali le drugega, razdaljo,
100. Prvi argument, self se doda avtomatsko - self bo enak objektu,
katerega metodo kličemo, v tem primeru t.
No, pa dodajmo še left in right. Še prej pa sprogramirajmo backward.
Tu nas popade lenoba. Za 42 korakov nazaj gremo lahko preprosto tako, da
gremo za -42 korakov naprej, ne? Metoda backward naj torej pokliče kar
forward.
def backward(self, a):
self.forward(-a)
Če hočemo poklicati metodo forward, moramo povedati tudi objekt, čigar
forward kličemo. Torej self.
Podobno kot backward uženimo še left in right, ki bosta prepustila delo
metodi turn. Motivacija je na prvi pogled manjša, saj bi lahko napisali
preprosto
def left(self):
self.angle -= 90
A ne bomo. Metoda turn, bo kmalu poskrbela še za kaj drugega (konkretno,
risanje želve), torej naj za to poskrbi tudi pri obračanju na levo in desno.
Naredili bomo torej tako:
def left(self):
self.turn(-90)
def right(self):
self.turn(90)
Le še nekaj drobnarij nam je ostalo: dviganje in spuščanje peresa, letenje in čakanje.
def pen_up(self):
self.pen_active = False
def pen_down(self):
self.pen_active = True
def fly(self, x, y, angle):
self.x, self.y = x, y
self.angle = angle
def wait(self):
risar.cakaj(s)
Popolna želva
V razred dodajmo še izris želve in čakanje: poučno bo.
Najprej izris. Želvo predstavimo z dvema krogoma, eden ima polmer 10, drugi,
ki predstavlja glavo, pa 4. Kroga - kot grafična objekta, takšna, s kakršnimi
smo se igrali prejšnjič - bomo shranili v self.body in self.head. Najprej
napišimo metodo, ki ju - ob predpostavki, da že obstajata - postavi na
ustrezna položaja.
def update(self):
phi = radians(self.angle)
self.body.setPos(self.x, self.y)
self.head.setPos(self.x + 5 * cos(phi), self.y - 5 * sin(phi))
Kot preračunamo tako, kot pri metodi forward. Središče velikega kroga mora
biti v self.x in self.y. Manjši krog, glavo, zamaknemo za pet točk v smeri
phi. Njegovo središče bo torej v
self.x + 5 * cos(phi), self.y - 5 * sin(phi), po enaki formuli, kot bi jo
uporabili za premik (forward) za pet točk.
Da bo to v resnici delovalo, moramo kroga sestaviti. To seveda storimo ob
inicializaciji, v funkciji __init__, ki ji za to dodamo
self.body = risar.krog(0, 0, 5, risar.zelena, 3)
self.head = risar.krog(0, 0, 2, risar.zelena, 3)
self.update()
Kroga smo postavili kar v (0, 0), potem pa takoj poklicali metodo update(),
ki ju prestavi, kamor sodita.
Smo že skoraj na cilju: kroga obstajata in imamo tudi funkcijo, ki ju postavi
na pravo mesto. Preostane nam le še, da funkcijo pokličemo vsakič, ko želva
spremeni svoje koordinate ali smer. Srečo imamo: ker smo lepo programirali,
moramo poklicati je dovolj, da pokličemo update na treh mestih, namreč na
koncu metod forward, turn in fly. V metodi backward nam je ni potrebno,
saj ta le pokliče forward, v left in right pa tudi ne, saj pokličeta
turn.
Za skrivanje in prikazovanje poskrbimo z metodama hide in show, ki ju
imajo risarjevi objekti (ali, da si ne lastim zaslug, ki jih nimam, PyQtjevi
objekti, ki se skrivajo za risarjem).
def show(self):
self.body.show()
self.head.show()
def hide(self):
self.body.hide()
self.head.hide()
Ko pokličemo self.body.hide(), veliki krog še vedno obstaja, še vedno se
premika naokrog ... le izriše se ne. S self.body.show() pa ga spet pokažemo.
Zdaj pa še čakanje. Koliko sekund naj želva počaka po vsakem koraku, naj pove
atribut self.pause. Če ima vrednost 0, ne čakamo; če manjšo od 0, pa bo
želva počakala, da uporabnik pritisne tipko.
Spremeniti moramo tole: v __init__ dodamo self.pause = 0. Želva naj ne
čaka; če bomo hoteli čakanje, ga bo potrebno vključiti. Poleg tega dopišemo
še metodi set_pause in no_pause, takole
def set_pause(self, s):
self.pause = s
def no_pause(self):
self.set_pause(0)
Vse je pripravljeno, dodati je potrebno le še čakanje samo. Tu se bomo znašli:
čakanje bomo dodali kar v update, h kateremu dodamo
if self.pause != 0:
self.wait(self.pause)