Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: Python ja olio-ohjelmointi lyhyesti

Pekka Karjalainen [05.05.2008 14:15:09]

#

Minulla oli lyhyt esimerkki käytössä ja vähän aikaa kirjoitella siihen liittyvää selitystä. Kuten aina, muidenkin putkisten näkemykset ovat tervetulleita. Tulos kirjoittelustani tulee nyt nähtäväksi. Tämä ikään kuin jatkaa toisessa säikeessä ollutta aihetta olio-ohjelmoinnista.

Tarkoitus on esittää lyhyesti Python-tyylisen dynaamisen OO:n peruskäsitteet ja antaa esimerkkikoodi, jolla niitä voi testata. Voi olla hyvä idea hypätä selityksen yli, ottaa koodi talteen ja kokeilla sitä, sekä lukea selitys sen ohella.

- - - -

Olio-ohjelmoinnin peruskäsitteitä tässä jutussa ovat luokat, instanssit, metodit, perintä ja abstraktit luokat. Tämän lisäksi on muitakin, mutta kaikkea ei voi kertoa kerralla. Ennen esimerkkikoodia määrittelen ne lyhyesti.

Luokkamääritelmä kertoo, minkälaisia tietyt oliot ovat. Se on kokoelma ohjeita siitä, miten jotakin ohjelmassa oikeasti esiintyviä asioita voidaan luoda, käsitellä ja tuhota. Pythonissa useimmat oliot tuhoutuvat itsestään, kun niitä ei tarvita, eikä tätä tarvitse ajatella perusohjelmia tehdessä ollenkaan. Tärkeämpää on miettiä, mitä toimintoja luokkiin liitetään.

Instanssit ovat olioita, jotka ovat tietyllä hetkellä oikeasti olemassa. Ikkunointia käyttävää ohjelmaa tehdessä voisi olla käytössä esim. luokat nimeltä Ikkuna ja Valikko. Tietyllä hetkellä ruudulla näkyvät ikkunat ja valikot, joita käyttäjä voisi värkätä, olisivat näiden luokkien instansseja. On siis yksi luokka Ikkuna, mutta vaihtuva määrä sen instansseja. Jos ikkunoita tarvitaan paljon, niitä voi olla olemassa useita yhtä aikaa.

Metodit ovat toimintoja, joita on laitettu luokkiin. Niiden kautta voidaan eri instanssia kanssa vuorovaikuttaa, eli niiden avulla voi instansseilla myös tehdä jotakin. Ikkunalla voisi olla vaikkapa metodit siirrä() ja suurenna(), jotta sen instansseja voisi siirrellä ja suurentaa. Jokainen metodi tietää, mihin instanssiin se kohdistetaan, ja käsittelee vain sitä.

Perintä tarkoittaa luokkien suhteita toisiinsa. Perinnässä on kaksi luokkaa, joista toinen on yläluokka (superclass) ja toinen alaluokka (subclass). Näistä alaluokka on peritty yläluokasta, ja se tarkoittaa yksinkertaisesti sitä, että alaluokka on tietynlainen versio yläluokasta, jonka toimintaa on jollakin tavalla tarkennettu. Esimerkiksi TekstiIkkuna, jossa on tekstikenttä voisi olla tavallisen Ikkunan alaluokka.

(Pythonissa on myös moniperintä, jossa luokalla voi useita yläluokkia. Ei mennä siihen nyt.)

Abstrakti luokka on luokka, jonka instansseja ei voi luoda. Se on olemassa vain järjestämässä perintähierarkiaa, koska abstrakteista luokista voi periä konkreettisia luokkia, joiden instansseja voi luoda. Esimerkiksi Ikkuna voisi olla abstrakti luokka, ja vain TekstiIkkuna, KuvaIkkuna jne. sellaisia, mitä oikeasti voi luoda ohjelmassa.

Kaikille ikkunoille yhteiset metodit voi tässä kätevästi määritellä vain kerran Ikkuna-yläluokassa, mikä olisikin sen olemassaolon tärkein peruste. Perinnän idea onkin juuri tämä. Koska kaikki ikkunat ovat samanlaisia, riittää kertoa kerran, millä tavalla ne ovat samanlaisia!

Sopiva perintäsuhteiden käyttö on vaikeaa aluksi. Onkin tärkeämpää keskittyä löytämään sopivat luokat ohjelmaa varten ja sitten vasta miettiä, kuinka laajat perintäsuhteet niiden välille voi löytää. Pythonissa ei ole pakko käyttää perintää laajemmissa ohjelmissakaan ollenkaan, vaikka oikein käytettynä se tietenkin säästää turhalta toistolta ja tekee ohjelman rakenteesta selkeämmän.

Pythonissa luokka määritellään sanalla class ja luokan nimi. Tätä seuraa suluissa luettelo luokista, joista tämä luokka perii ominaisuuksia. On suositeltavaa periä kaikki ylimmät luokat perusluokasta object.

class Koira (Elain):

Tämä sanoo siis, että Koira on luokka, jonka yläluokka on Elain. Elain täytyy siis myös määritellä.

Luokkiin kuuluu ns. sisäänrakennettuja metodeja, joiden nimen ympärillä on kaksi alaviivaa tällä tavalla __metodi__. Näitä on useita, mutta tärkein on __init__, joka luo uuden luokan instanssin.

def __init__ (self, nimi):
    self.nimi = nimi

Tämä määrittelee luokan metodin __init__, jonka avulla luodaan uusi instanssi. Ensimmäisen parametrin nimi on self sopimuksen takia, ja sitä nimeä suositellaan (vaikkei ihan pakko olekaan) käyttämään. Sen jälkeen seuraa nolla tai useampia parametreja, jotka kertovat minkälainen olio halutaan luoda. Vain self on pakollinen. Tässä on yksi lisäparametri: nimi.

Rivi self.nimi = nimi asettaa parametrin arvon tämän tietyn instanssin nimikenttään. Ensimmäinen nimi-sana viittaa olion omaan kenttään ja toinen parametriin. Niille voi siis antaa saman nimen, vaikka ne tarkoittavat eri asioita, koska self.nimi on osa self-muuttujaa.

Tätä __init__-metodia vastaava toiminto olisi Hurtta-nimisen Koira-olion luonti:

koira = Koira ("Hurtta")

Koska Koiraa luodessa on yksi parametri, merkkijono "Hurtta", kutsutaan __init__-metodia kahdella parametrilla. Ensimmäinen on juuri luotu instanssi, johon viitataan self-sanalla ja toinen on luonnissa varsinaisesti annettu parametri. __init__ saa siis aina yhden parametrin enemmän (self), kuin mitä luonnissa on käytetty.

Tämän jälkeen koira.nimi on merkkijono "Hurtta", mutta muilla luokan Koira instansseilla on yhä oma nimensä.

On hyvä tapa rajoittaa kenttien määrittely __init__-metodiin, aina kun se on järkevää. Niitä voi lisätä missä kohdassa ohjelmaa tahansa, mutta tätä vapautta on helppo käyttää väärin ja tehdä ohjelmasta vaikean ylläpitää ja muokata. Kun määrittelet uuden luokan, mieti heti miltä sen __init__ näyttäisi.

(Huomaa, että luokan määrittely isolla kirjaimella mahdollistaa tavallisen Koira-luokkaan viittavan muuttujan määrittelyn pienellä kirjaimella. Muuttuja koira on siis vain jokin koira (juuri nyt nimeltään "Hurtta"), kun taas (class) Koira on luokka.)

Lisäksi luokassa voi olla omia metodeja, joille voi antaa tavalliset nimet. Esimerkkikoodissa niitä ovat kuvaile() ja sano(). Myös ne saavat aina ensimmäisenä parametrina self-muuttujan, joka kertoo, mikä on se instanssi, mitä nyt käsitellään.

Perintä astuu kuvioihin mukaan kun kenttiä ja metodeja haetaan. Jos luokasta itsestään ei löydy jotakin kenttää, se haetaan sen yläluokista. Esimerkkikoodissa jokaisella Elain-luokasta perityllä luokalla siis on käytännössä kuvaile()-metodi, vaikka sitä ei ole määritelty niissä kaikissa. Siellä, missä se määrittellään uudestaan, se korvaa vanhemman määrittelyn; sanotaan myös, että uusi määrittely syrjäyttää (override) vanhan. Tämä tehdään esimerkin vuoksi Kissa-luokassa.

Joskus halutaan myös tarkoituksella käyttää jotain yläluokan metodia. Tämä onnistuu hieman vaikean näköisesti super-kutsulla, jota on käytetty esimerkkiohjelman __init__-metodeissa. Kaikki Elain-luokan alaluokat kutsuvat Elain-luokan __init__-metodia antaakseen itselleen nimen. Tämä ei ole niinkään järkevää juuri tässä ohjelmassa, vaan on tässä näyttämässä, että se on mahdollista. Kannattaa myös muistaa, että jokaisen perityn alaluokan oma __init__ voi myös tehdä omaa luokkakohtaista alustusta tämän super-kutsun lisäksi.

Koska jokainen elukat-listan olioista näin ollen tuntee metodin kuvaile(), voidaan for-silmukassa kutsua sitä jokaiselle. Jokainen eläin kuvailee itsensä, ja koska Kissa on syrjäyttänyt kuvaile()-metodin omallaan, se kuvailee itseään eri tavalla kuin muut.

Nyt kuvaile-metodissa viitataan metodiin sano(), jota ei olekaan yläluokassa Elain. Eikö tämä ole ongelma? Eipä näytä olevan, koska kuvaile()-metodia kutsutaan vain eläimille, jotka ovat sen määritelleet. Ongelmana kuitenkin on, että joku voi luoda Elain-luokan instanssin, ja se ei voikaan kuvailla itseään. Tästä on esimerkki ohjelman lopussa, missä tapahtuu sen takia virhe. (try ja except huolehtivat virheen eli poikkeuksen sieppaamisesta ja kuvailusta)

Saattaisi olla hyvä idea tehdä Elain-luokasta abstrakti, ettei sitä voisi instantioida edes vahingossa. Tämä onnistuukin, ja koodissa on toinen toteutus kommentteissa, jossa tehdään näin. Sen voi ottaa käyttöön poistamalla #-kommenttimerkin metodeissa olevista koodiriveistä ja kommentoimalla vastaavasti pois näin saatuja rivejä edeltävät rivit. Kun sen tekee, luo jokainen olio nyt itselleen nimi-kentän ja yritys luoda Elain aiheuttaa jo virheen. Idea on, että __init__-metodia ei toteuteta ollenkaan Elain-luokassa, vaan se nostaa poikkeuksen.

Tämä on parempi asia kuin se, että virhe tapahtuu vasta, kun laitonta Elain-instanssia yritetään saada kuvailemaan itseään. Järjetön operaatio (eli eläimen luominen, jonka lajia (tarkkaa luokaa) ei tiedetä) voidaan merkitä virheeksi, joten se ei jää huomaamatta ohjelmaan aiheuttamaan bugeja.

Olisi myös mahdollista tehdä sano()-metodi Elain-luokkaan, joka samalla tavalla nostaisi NotImplemented-poikkeuksen. Tässäkin kuitenkin virheen paljastuminen viivästyisi, mikä ei ole toivottavaa.

Pythonissa siis NotImplemented-poikkeuksen käyttö on tapa ilmoittaa, että jokin luokka tai metodi on abstrakti, eikä sitä voi käyttää. Tällaiset metodit pitää syrjäyttää alaluokissa määritellyillä metodeilla, jolloin saadaan oikeita konkreettisia luokkia.

Nyt sitten kokeilemaan ja muokkaamaan koodia. Harjoituksena voit tehdä Hevonen-luokan ja lisätä sen instanssin hirnumaan elukat-listaan!

# -*- coding:latin-1 -*-
# Esimerkkiohjelma Pythonin OO:n perusteista Putkaa varten.
# Oheisessa tekstissä selityksiä ja lisää tietoa.

class Elain (object):
    def __init__ (self, nimi):
        self.nimi = nimi
        # raise NotImplementedError ("Elain on abstrakti luokka")

    def kuvaile (self):
        print
        print "Nimeni on", self.nimi
        print "Ääntelen sanomalla",
        self.sano()

class Sika (Elain):
    def __init__ (self, nimi):
        super(Sika, self).__init__(nimi)
        # self.nimi = nimi

    def sano (self):
        print "nöf nöf"

class Lehma (Elain):
    def __init__ (self, nimi):
        super(Lehma, self).__init__(nimi)
        # self.nimi = nimi

    def sano (self):
        print "muuuu"

class Koira (Elain):
    def __init__ (self, nimi):
        super(Koira, self).__init__(nimi)
        #  self.nimi = nimi

    def sano (self):
        print "vuh vuh"

class Kissa (Elain):
    def __init__ (self, nimi):
        super(Kissa, self).__init__(nimi)
        # self.nimi = nimi

    def kuvaile (self):
        print
        print "Olen kissa. Nimeni on", self.nimi,"ja nau'un, jos jaksan."
        print "Nyt ei huvita."

elukat = [ Sika ("Putte"), Lehma ("Mansikki"), Koira ("Peni"),
           Koira ("Musti"), Koira ("Rekku"), Kissa ("Mirri") ]

for elain in elukat:
    elain.kuvaile ()

virhe = None

try:
    virhe = Elain ("Outo otus")
except NotImplementedError, e:
    print
    print "! Virhe tuli, ei voida luoda luokan Elain oliota,"
    print "koska luokassa ei ole toimivaa __init__-metodia."
    print "Poikkeus on:", e

try:
    if virhe is not None: virhe.kuvaile ()
except AttributeError, e:
    print
    print "! Virhe tuli, kuten odotettiin. Luokan Elain instanssi"
    print "ei voi kuvailla itseään puuttuvan sano()-metodin takia."
    print "Poikkeus on:", e

# loppu

Antti Laaksonen [05.05.2008 21:04:30]

#

Hyvä selostus! Tästä ei olisi pitkä matka oppaaseen...

Chiman [05.05.2008 21:14:24]

#

Hyvä, selkeä esitys.

Pienenä sivuhuomiona mainitsen, että Pythonissa ei ole tapana jättää välilyöntiä funktion/luokan nimen ja sitä seuraavan sulun väliin. (PEP 8 -- Style Guide for Python Code. Tuo ohjeistus tosin koskee vain standardikirjastoa, mutta nähdäkseni nuo samat - ja hyvät, IMHO - periaatteet ovat laajalti käytössä muuallakin.)

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta