Definiranje novega razreda
Današnja tema je objektno orientirano programiranje (ali, po slovensko, a redkeje uporabljeno, predmetno usmerjeno programiranje). Nekaj terena smo pripravili prejšnjič, v resnici pa med objekti živimo že dolgo, le vedeli nismo zanje, čeprav smo samo besedo "objekt" sem ter tja sramežljivo izrekli. No, v pogostejšo rabo je prišla, ko smo tačeli govoriti o objektih in imenih, a šele danes bo dobila svoj pravi, dokončni pomen.
Na zadnjem predavanju smo risali, a omenili, da v resnici ne rišemo, temveč postavljamo objekte na sliko. Za risanje poskrbi Qt. Vsak objekt na sliki je določene vrste in ima določene lastnosti. Nekatere lastnosti so skupne več vrstam objektov, spet druge so smiselne le za določeno vrsto, zato jo poznajo le ti.
Vrste objektov, s katerimi smo se igrali, so bile črte, točke, krogi in
slike. Vsi so imeli koordinate: izvedeli smo jih z metodama
x()
in y()
ter spreminjali z metodo
setPos(x, y)
. Barvo in debelino pa so imele samo črte in
krogi, slike
pa ne (le metod za nastavljanje barv in debelin vam nisem pokazal, saj
stvar ne gre čisto naravnost, s kakimi setColor
in setWidth
).
Z metodami smo se pravzaprav prvič srečali že davno, ob seznamih. "Lastnost"
seznama so pač elementi, ki jih vsebuje, metode, ki jih premore, pa so,
recimo, append
, count
in index
.
Lastnost niza je prav tako kar njegova vsebina, metode, ki jih ima, pa
so, recimo, split
, replace
in
startswith
. Kaj so lastnosti datoteke, točneje,
spremenljivke tipa file
nam je težje uganiti (slutimo
pač že: spremenljivka vrste file
je nekako vezana na neko
datoteko na disku, torej je lastnost ta "povezava", poleg tega pa si
mora zapomniti vsaj še, do kje je datoteka že prebrana).
Danes bomo prvič sestavili nov podatkovni tip, novo vrsto spremenljivke.
"Tipom" - kot so na primer int
, float
, list
,
file
in QGraphicsItemLine
- pravimo razredi in
vrednostim - posamezni številki, seznamu, datoteki, črti - pravimo
objekti. Naš današnji podatkovni tip bo Turtle
.
Nekaj o jeziku programov: doslej smo programirali tako, da smo imena spremenljivk in funkcij pisali v slovenščini. Dasiravno je to, kljub svojih arhaizmom in celo prav zavoljo njih, moj najljubši jezik, vam priporočam: programirajte v angleščini. Š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
Objekt vrste Turtle
naredimo tako, da rečemo, recimo
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, naj 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 setPause(t)
. Pri tem
t
spet pove čas v sekundah. Če se čakanja naveličamo,
pokličemo noPause()
.
Ž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)
pojdis
točk naprej oz. nazajturn(phi)
obrni se zaphi
stopinj v smeri urinega kazalcaleft(), right()
obrni se za 90 stopinj levo oz. desnofly(x, y, phi)
poleti na koordinatix
,y
in se obrni v smerphi
pen_up(), pen_down()
dvigni oz. spusti perowait(t)
počakajt
sekundset_pause(t), no_pause()
nastavi oz. prekliči čakanje po vsakem ukazuhide(), show()
pokaži oz. skrij želvo
V vsej svoj prizemeljski preprostosti 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:
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. Na spodnjih slikah je 5 kvadratov, zasukanih
za 72 stopinj in 90 kvadratov zasukanih za 4 stopinje.
|
|
|
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.
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
.
l
,
pri mnogokotniku pa je l
dolžina stranice.
Ob snežinki navrzimo, brez posebne razlage, š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.
|
En krak, f = 1.4 |
f = 2 |
|
f = 1.4 |
Najlepše snežinke pa nam z želvo skuha Koch.
Dovolj igranja. Da je želva koristna žival, sem vas menda prepričal. Zdaj pa jo sprogramirajmo.
Trop želv
Še prej pa naredimo le še eno 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 deset točk naprej.
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
iny
- kam je obrnjena: to bomo shranili v
angle
- ali je pero spuščeno ali dvignjeno; to bomo shranili v
pen_active
, ki bo imel vrednostTrue
aliFalse
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
. Atribut so nekateri želeli sloveniti v
lastnost, pa se ni prijelo. V nekaterih programskih jezikih se skoraj
isti stvari reče polje ali, po angleško field.
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.
self
in ji
postavi self.x
in self.y
na sredo, obrne jo
navzgor (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 (s
).
Matematika nam naredi več dela kot programiranje. Najprej moramo
spremeniti self.angle
v kot, s kakršnim dela računalnik.
Glede tega, namreč kota, si smemo čestitati: z njim je narobe natanko
vse, kar more biti narobe; je v napačnih enotah (stopinje namesto
radianov), 0 kaže v napačno smer (gor namesto desno) in teče v napačno
smer (povečuje se v smeri urinega kazalca namesto obratno). Pretvarjanje
iz stopinj v radiane prepustimo funkciji radians
, naši
stari znanki iz "topologije". Da uredimo težavo z ničlo in orientacijo,
pa ga odštejmo od 90; 90 poskrbi za začetno smer, minus pa obrne urin
kazalec.
Nato v nx
in ny
izračunamo, kam je potrebno
prestaviti želvo. V smeri x se premaknemo za s * cos(angle) v y
za s * sin(angle). 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.
Napišimo še eno funkcijo: obračanje želve. Ta je trivialna in nevredna komentarja.
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.
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
self
. (V nekaterih jezikih obstaja
this
, ki se od Pythonovega self
razlikuje po
dveh značilnostih: prva je, da nam ga navadno ni potrebno omenjati,
druga pa, da mu je vedno ime this
. V Pythonu ga
moramo omeniti med argumenti, ime pa je načelno poljubno.)
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
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
s
. 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
.
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
.
forward
,
moramo povedati tudi objekt, čigar forward
kličemo. Torej
self
. forward
ni funkcija kar tako. (Nekatere
bo to motilo, tudi mene je v začetku, saj sem pred Pythonom znal C++, ki
uporablja "krajšo" varianto klicanja metod. Zdaj pa me pravzaprav moti
način, v katerem je to narejeno v C++. V Pythonu je iz načina klicanja
očitno, da je forward
metoda. V C++ pa ni na prvi pogled
očitno, ali je forward
metoda razreda ali pa morda neka
funkcija; da to razčistimo, moramo pogledati v definicijo razreda.
Podobno je s self.x
, namesto katerega bi v nekaterih
jezikih pisali kar x
. V Pythonu je očitno, da ne govorimo o
nekem x
-u kar tako, temveč o atributu, polju objekta.)
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
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:
Le še nekaj drobnarij nam je ostalo: dviganje in spuščanje peresa, letenje in čakanje.
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.
Kot preračunamo tako, kot smo se (nam)učili 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 angle
. Njegovo središče bo
torej v self.x+5*cos(angle), self.y-5*sin(angle)
, 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
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).
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
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
resničen
ravno takrat, kot je self.pause
različen od 0.
Varstvo osebnih podatkov želve
Med pravila lepega vedenja pri programiranju objektov (točneje, razredov) sodi tudi skrivanje podatkov ali, v Pythonu, ki je bolj liberalen jezik, "spoštovanje zasebnosti". Vzemimo želvo Ano.
Turtle
dodamo funkcije, kot so
Za tole sicer obstaja boljši mehanizem, vendar se o njem pri Programiranju 1 ne bomo učili. Tisti, ki bi radi znali, bodo pogledali dekorator property.
Še bolj pomembno kot to, da ne škilimo v želvine osebne podatke, je, da za
prestavljanje uporabljamo metode, kot je fly
.
Razlogi za tole so filozofsko-načelno-praktične narave. Kot prvo, kaka
prihodnja verzija želve bo morda shranjevala koordinate na kak drug
način. S tem, ko omogočimo dostop do podatkov le prek funkcij, moramo za
zagotavljanje združljivosti poskrbeti le, da imajo funkcije enaka imena,
kar se dogaja znotraj želve, pa lahko poljubno spreminjamo.
Drugi: želva je objekt ter mora vedeti in nadzorovati, kaj se dogaja z
njo, ne pa, da drugi od zunaj počno z njo, kar hočejo. Ko jo, na primer,
prestavimo, mora želva vedeti, da smo jo prestavili, tako da se lahko
nariše na novi lokaciji. Če koordinate spreminjamo od zunaj, to ne
sproži metode update
.
V določenih drugih jezikih (pravzaprav najbrž kar v večini objektnih jezikov) lahko celo naročite, naj bodo določeni podatki skriti "outsiderjem". To se šteje za zelo dobro prakso in to velja početi. V drugem semestru se boste učili Javo in gotovo izvedeli veliko o tem. Tudi v Pythonu je mogoče te reči delati po pravilih; vaš predavatelj pa je en pacek. Takih stvari se mu ne ljubi početi in doslej ga še ni dovolj teplo, da bi ga izučilo. Povem vam le, da boste vedeli.