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 x in y,
  • kam je obrnjena: to bomo shranili v angle,
  • ali je pero spuščeno ali dvignjeno; to bomo shranili v pen_active, ki bo imela vrednost True ali False
Na izrisovanje želv in čakanje po vsakem koraku za zdaj še pozabimo.

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)            
Last modified: Wednesday, 27 April 2016, 10:37 PM