Luokan määrittely ja olioiden käsittely

Jos haluamme tehdä ohjelman, joka käsittelee opiskelijaolioita, on ohjelmassa jotenkin määriteltävä se, millaisia opiskelijaoliot ovat ja mitä toimenpiteitä niille voi tehdä. Tämä tehdään luokassa, jonka nimi on Opiskelija. Kun on kirjoitettu Opiskelija-luokka, voidaan luoda Opiskelija-olioita ja suorittaa niille erilaisia toimenpiteitä.

Yleisesti siis luokassa (engl. class) määritellään, millaisia ominaisuuksia sen luokan olioilla (engl. object) on ja mitä toimenpiteitä luokan olioille voi tehdä. Luokkaa voidaan tavallaan verrata jonkin koneen piirustuksiin ja olioita piirustusten perusteella rakennettuihin koneisiin. Samoin kuin yksien piirustusten perusteella voidaan rakentaa monta konetta, yhdestä luokasta voidaan luoda monta oliota.

Tarkastelemme opiskelijaesimerkin avulla, miten luokka kirjoitetaan ja miten luokan olioita voidaan luoda ja käyttää. Kenttien avulla kerrotaan, mitä ominaisuuksia luokan olioilla on ja mikä on jonkin ominaisuuden arvo tietyllä oliolla. Esimerkiksi tässä määriteltävällä Opiskelija-oliolla on kentät __nimi, __opiskelijanumero, __tenttiarvosana ja __harjoitusarvosana, joiden arvoksi voidaan tallentaa käsiteltävän Opiskelija-olion nimi, opiskelijanumero sekä tentti- ja harjoitusarvosana vastaavasti. Jokaisella luotavalla oliolla on omat kenttien arvot, jotka eivät riipu toisten saman luokan olioiden kenttien arvoista. Olion kenttien arvot säilyvät niin kauan kuin olio on olemassa (ne eivät siis häviä esimerkiksi jostain funktioista poistuttaessa), ellei ohjelmassa erikseen muuteta jonkin kentän arvoa. Edellä luetellut kenttien nimet alkavat kahdella alaviivamerkillä (__). Syy tähän kerrotaan vähän myöhemmin.

Olion kenttiä voi käyttää luokan sisällä samaan tapaan kuin tavallisia muuttujia. Kentän nimen edessä kuitenkin kerrotaan aina, minkä olion kentästä on kysymys. Tämä tehdään pistenotaation avulla. Jos halutaan käsitellä muuttujan oliomuuttuja viittaman olion kenttää kentta, kirjoitetaan oliomuuttuja.kentta. Usein luokan sisällä olevissa metodeissa käsitellään sitä oliota, jota ollaan juuri luomassa tai jolle on juuri kutsuttu luokan jotain metodia. Tällaiseen olioon viitataan nimellä self. Tätä asiaa selvennetään lisää, kun käydään läpi luokan Opiskelija sisältöä.

Luokan määrittely aloitetaan otsikolla, johon kuuluu sana class ja luokan nimi. Sen jälkeen tulee kaksoispiste. Yleinen tapa on aloittaa luokan nimi isolla kirjaimella.

class Opiskelija:

Seuraavien rivien sisennysten avulla osoitetaan, mitkä rivit kuuluvat tämän luokan määrittelyyn. Luokan määritettelyssä kerrotaan, millaisia toimenpiteitä luokan olioille voidaan suorittaa. Tämä määritellään metodien avulla. Samaan tapaan kuin funktio, metodi on kappale koodia, jolle on annettu oma nimi. Metodi liittyy kuitenkin aina johonkin luokkaan ja sitä kutsutaan aina jollekin tämän luokan oliolle. Metodia kutsutaan vähän eri tavalla kuin funktioita. Aikaisemmin on ollut jo esimerkkejä siitä, kuinka Pythonin valmiita metodeita on kutsuttu esimerkiksi listoja, merkkijonoja ja tiedostoja käsiteltäessä. Nyt olioiden yhteydessä tutustumme siihen, miten metodeita voi kirjoittaa itse.

Luokan määrittelyssä kerrotaan yleensä aluksi, mitä tehdään, kun luodaan uusi tämän luokan olio. Ohjelmoijan on kerrottava esimerkiksi se, millaisia alkuarvoja annetaan olion kentille. Tämä tehdään metodissa, jonka nimi on __init__ (Nimen alussa ja lopussa on kaksi alaviivamerkkiä _.) Kaikilla metodeilla on ensimmäinen parametri, jonka nimi on self. Tämä tarkoittaa sitä oliota, jota ollaan juuri luomassa tai käsittelemässä. Tämän lisäksi metodilla voi olla muita parametreja, joiden avulla metodille annetaan tietoa käytettävistä arvoista. Esimerkiksi metodin __init__ parametrien avulla kerrotaan usein, mitä alkuarvoja luotavan olion kentille annetaan.

Kirjoitetaan luokan Opiskelija metodi __init__ siten, että luotavalle Opiskelija-oliolle annettava nimi ja opiskelijanumero annetaan metodin parametrina. Uuden opiskelijan tentti- ja harjoitusarvosanoiksi asetetaan aluksi 0.

def __init__(self, annettu_nimi, numero):
    self.__nimi = annettu_nimi
    self.__opiskelijanumero = numero
    self.__tenttiarvosana = 0
    self.__harjoitusarvosana = 0

Tässä siis ilmaus self.kentan_nimi tarkoittaa sen Opiskelija-olion kenttää, jota ollaan juuri luomassa. Kun Opiskelija-luokkaan on kirjoitettu tällainen __init__-metodi, voidaan uusia Opiskelija-olioita luoda seuraavaan tapaan (yleensä luominen tapahtuu luokan ulkopuolella, esimerkiksi pääohjelmassa):

kurssilainen1 = Opiskelija("Mia Lahti", "608012")

Tässä kurssilainen1 on muuttuja, joka pannaan sijoituskäskyllä viittaamaan luotavaan olioon. Sen kautta on myöhemmin mahdollisuus päästä käsittelemään nyt luotavaa oliota. Sijoituskäskyn oikealla puolella oleva osa saa aikaan varsinaisen olion luonnin. Python-tulkki luo uuden olion ja suorittaa sitten __init__-metodissa olevat käskyt niin, että parametrin annettu_nimi arvo on merkkijono "Mia Lahti" ja parametrin numero arvo merkkijono "608012". Metodin __init__ ensimmäiselle parametrille self ei anneta oliota luodessa arvoa, vaan parametrilla tarkoitetaan sitä oliota, jota ollaan juuri luomassa.

Tarkastellaan sitten luokan muita metodeja. Jotta luodun Opiskelija-olion tentti- ja harjoitusarvosanoja voisi myöhemmin muuttaa, määritellään metodit tentti- ja harjoitusarvosanojen muuttamista varten. Alla on kirjoitettu metodi, jonka avulla voi muuttaa olion __tenttiarvosana-kenttää. Metodi saa parametrina opiskelijalle annettavan uuden tenttiarvosanan. Ennen kentän arvon muuttamista metodi tarkistaa, että parametri on sallitulla välillä 0–5. Jos parametri ei ole tällä välillä, metodi ei tee mitään. Silloin käsiteltävän olion vanha __tenttiarvosana-kentän arvo jää voimaan.

def muuta_tenttiarvosana(self, arvosana):
    if 0 <= arvosana <= 5:
        self.__tenttiarvosana = arvosana

Metodin ensimmäinen parametri self tarkoittaa sitä Opiskelija-oliota, jolle metodia ollaan kutsumassa. Jos on aikaisemmin luotu Opiskelija-olio ja pantu muuttuja kurssilainen1 viittaamaan siihen, voidaan metodia kutsua seuraavasti:

kurssilainen1.muuta_tenttiarvosana(4)

Usein ohjelmassa on luotu useita saman luokan olioita. Voitaisiin esimerkiksi luoda toinen Opiskelija-olio ja panna muuttuja kurssilainen2 viittaamaan siihen kirjoittamalla

kurssilainen2 = Opiskelija("Niko Virtanen", "588123")

Silloin tämän jälkimmäisen opiskelijan tenttiarvosanaa voitaisiin muuttaa kirjoittamalla

kurssilainen2.muuta_tenttiarvosana(3)

Metodin kutsussa ennen pistettä oleva muuttuja määrää sen, mille oliolle metodia kutsutaan. Aikaisemmin esitetty ensimmäinen muuta_tenttiarvosana-kutsu muutti Mia Lahden tenttiarvosanaa, kun taas yllä esitetty kutsu muuttaa Niko Virtasen tenttiarvosanaa. Jokaisella oliolla on omat kopionsa kenttien arvoista ja muutokset yhden olion kentän arvossa eivät mitenkään vaikuta toisten saman luokan olioiden kenttien arvoihin.

Kannattaa huomata, että metodien kutsuissa ei ole lainkaan sulkujen sisällä annettu käsiteltävää oliota eli parametrin self arvoa. Se on annettu kutsussa jo ennen pistettä ja metodin nimeä. Sen sijaan metodin muiden parametrien arvot on annettu ihan samalla tavalla kuin funktioillakin.

Seuraava animaatio vielä esittää olioiden luomista ja niiden metodien toimintaa. Animaatiossa esitetyssä koodissa on käytetty opiskelijoilla lyhyempiä nimiä ja opiskelijanumeroita, jotta oliot mahtuisivat paremmin näytölle. Samasta syystä myös metodin muuta_tenttiarvosana nimi on muutettu muotoon muuta_tentti. Animaatiossa on vain osa Opiskelija-luokan koodista.

Metodi muuta_harjoitusarvosana kirjoitetaan samalla tavalla kuin muuta_tenttiarvosana. Ainoastaan muutettavan kentän nimi on eri.

Kirjoitetaan sitten luokkaan metodi, jonka avulla voidaan laskea opiskelijan kokonaisarvosana. Se lasketaan tentti- ja harjoitusarvosanojen keskiarvona. Aluksi kuitenkin tarkistetaan, onko toisen tai molempien osasuoritusten arvosana 0. Siinä tapauksessa kokonaisarvosanaksi tulee 0 keskiarvosta riippumatta. Metodi palauttaa arvonaan lasketun keskiarvon. Metodin paluuarvoa voidaan käyttää hyväksi aivan samalla tavalla kuin funktioiden paluuarvoja.

def laske_kokonaisarvosana(self):
    if self.__tenttiarvosana == 0 or self.__harjoitusarvosana == 0:
        arvosana = 0
    else:
        arvosana = (self.__tenttiarvosana +
                    self.__harjoitusarvosana + 1) // 2
    return arvosana

Keskiarvon laskemisessa lisätään osasuoritusarvosanojen summaan ykkönen. Näin saadaan aikaan se, että puolet numerot pyöristyvät ylöspäin. Laskemisessa suoritetaan kokonaislukujen jakolasku, jolloin saatu arvosana on aina kokonaisluku.

Alla on esimerkki metodin kutsumisesta ja sen paluuarvon tulostamisesta. (Oletetaan jälleen, että ohjelmassa on luotu aikaisemmin mainitut oliot.)

print("Nikon kurssiarvosana on", kurssilainen2.laske_kokonaisarvosana())

Mainittujen metodien lisäksi luokkaan määritellään metodeita, joiden avulla voi luokan ulkopuolelta selvittää jonkin kentän arvon. Nämä metodit siis vain palauttavat yhden kentän arvon eivätkä tee mitään muuta. Alla esimerkkinä metodit kerro_nimi ja kerro_tenttiarvosana. Myös kahdelle muulle kentälle määritellään vastaavat metodit.

def kerro_nimi(self):
    return self.__nimi


def kerro_tenttiarvosana(self):
    return self.__tenttiarvosana

Seuraavaksi koko luokan määrittely yhtenä kokonaisuutena. Määrittelyyn on lisätty myös kommentit.

# Luokka Opiskelija kuvaa eraan ohjelmointikurssin
# yhta opiskelijaa.

class Opiskelija:

    # Metodi __init__ antaa alkuarvot olion kentille.
    # Luotavalle opiskelijalle annettava nimi ja opiskelijanumero
    # annetaan metodin parametreina.

    def __init__(self, annettu_nimi, numero):
        self.__nimi = annettu_nimi
        self.__opiskelijanumero = numero
        self.__tenttiarvosana = 0
        self.__harjoitusarvosana = 0


    # Metodi palauttaa opiskelijan nimen.

    def kerro_nimi(self):
        return self.__nimi


    # Metodi palauttaa opiskelijan opiskelijanumeron.

    def kerro_opiskelijanumero(self):
        return self.__opiskelijanumero


    # Metodi palauttaa opiskelijan tenttiarvosanan.

    def kerro_tenttiarvosana(self):
        return self.__tenttiarvosana


    # Metodi palauttaa opiskelijan harjoitusarvosanan.

    def kerro_harjoitusarvosana(self):
        return self.__harjoitusarvosana


    # Metodi muuttaa opiskelijan tenttiarvosanan. Uusi arvosana
    # annetaan metodin parametrina.

    def muuta_tenttiarvosana(self, arvosana):
        if 0 <= arvosana <= 5:
            self.__tenttiarvosana = arvosana


    # Metodi muuttaa opiskelijan harjoitusarvosanan. Uusi arvosana
    # annetaan metodin parametrina.

    def muuta_harjoitusarvosana(self, arvosana):
        if 0 <= arvosana <= 5:
            self.__harjoitusarvosana = arvosana


    # Metodi laskee ja palauttaa opiskelijan kokonaisarvosanan.

    def laske_kokonaisarvosana(self):
        if self.__tenttiarvosana == 0 or self.__harjoitusarvosana == 0:
            arvosana = 0
        else:
            arvosana = (self.__tenttiarvosana +
                        self.__harjoitusarvosana + 1) // 2
        return arvosana

Seuraava pääohjelma luo kaksi Opiskelija-oliota niin, että luotavien olioiden tiedot kysytään käyttäjältä. Ohjelma laskee opiskelijoiden kokonaisarvosanat ja tulostaa ne sekä opiskelijoiden muut tiedot. Arvosanojen lukemiseen käyttäjältä on määritelty oma apufunktio. Näin mahdollisten virheellisten syötteiden käsittely voidaan hoitaa helposti. Ohjelmaa kirjoittaessa on oletettu, että pääohjelma on samassa tiedostossa luokan Opiskelija kanssa. Jos pääohjelma on kirjoitettu toiseen tiedostoon (kuten usein käytännössä tehdään), pitää siihen tehdä pieniä lisäyksiä, joista kerrotaan tarkemmin vähän myöhemmin.

def lue_kokonaisluku():
    luku_onnistui = False
    while not luku_onnistui:
        try:
            luku = int(input())
            luku_onnistui = True
        except ValueError:
            print("Virheellinen kokonaisluku!")
            print("Anna uusi!")
    return luku

def main():
    nimi1 = input("Anna 1. opiskelijan nimi: ")
    op_nro1 = input("Anna 1. opiskelijan opiskelijanumero: ")
    kurssilainen1 = Opiskelija(nimi1, op_nro1)
    nimi2 = input("Anna 2. opiskelijan nimi: ")
    op_nro2 = input("Anna 2. opiskelijan opiskelijanumero: ")
    kurssilainen2 = Opiskelija(nimi2, op_nro2)

    print("Anna 1. opiskelijan tenttiarvosana.")
    tentti1 = lue_kokonaisluku()
    kurssilainen1.muuta_tenttiarvosana(tentti1)
    print("Anna 1. opiskelijan harjoitusarvosana.")
    harjoitus1 = lue_kokonaisluku()
    kurssilainen1.muuta_harjoitusarvosana(harjoitus1)

    print("Anna 2. opiskelijan tenttiarvosana.")
    tentti2 = lue_kokonaisluku()
    kurssilainen2.muuta_tenttiarvosana(tentti2)
    print("Anna 2. opiskelijan harjoitusarvosana.")
    harjoitus2 = lue_kokonaisluku()
    kurssilainen2.muuta_harjoitusarvosana(harjoitus2)

    print("1. opiskelijan tiedot:")
    print(kurssilainen1.kerro_opiskelijanumero())
    print(kurssilainen1.kerro_nimi())
    print("Tenttiarvosana:", kurssilainen1.kerro_tenttiarvosana())
    print("Harjoitusarvosana:",
          kurssilainen1.kerro_harjoitusarvosana())
    print("Kurssiarvosana:", kurssilainen1.laske_kokonaisarvosana())

    print("2. opiskelijan tiedot:")
    print(kurssilainen2.kerro_opiskelijanumero())
    print(kurssilainen2.kerro_nimi())
    print("Tenttiarvosana:", kurssilainen2.kerro_tenttiarvosana())
    print("Harjoitusarvosana:",
          kurssilainen2.kerro_harjoitusarvosana())
    print("Kurssiarvosana:", kurssilainen2.laske_kokonaisarvosana())


main()

Seuraavaksi esimerkki ohjelman suorituksesta:

Anna 1. opiskelijan nimi: Minni Hiiri
Anna 1. opiskelijan opiskelijanumero: 11223F
Anna 2. opiskelijan nimi: Hessu Hopo
Anna 2. opiskelijan opiskelijanumero: 33441C
Anna 1. opiskelijan tenttiarvosana.
3
Anna 1. opiskelijan harjoitusarvosana.
5
Anna 2. opiskelijan tenttiarvosana.
tekstia
Virheellinen kokonaisluku!
Anna uusi!
1
Anna 2. opiskelijan harjoitusarvosana.
2
1. opiskelijan tiedot:
11223F
Minni Hiiri
Tenttiarvosana: 3
Harjoitusarvosana: 5
Kurssiarvosana: 4
2. opiskelijan tiedot:
33441C
Hessu Hopo
Tenttiarvosana: 1
Harjoitusarvosana: 2
Kurssiarvosana: 2

Usein jokainen luokka kirjoitetaan omaan moduuliinsa (käytännössä omaan tiedostoonsa) ja myös pääohjelma eri moduuliin kuin luokkien määrittelyt. Näin tehdään esimerkiksi siksi, että omassa moduulissa olevaa luokkaa on helppo käyttää hyväksi muissakin ohjelmissa kuin siinä, jota varten se on alunperin tehty. Jos Opiskelija-luokka kirjoitetaan omaan moduuliinsa opiskelija (tiedoston nimeksi tulee tällöin opiskelija.py) ja pääohjelma toiseen moduuliin, on pääohjelmatiedoston alkuun kirjoitettava rivi

import opiskelija

jotta toisessa moduulissa oleva luokka saataisiin käyttöön. Lisäksi Opiskelija-olioita luodessa on annettava luokan sisältävän moduulin nimi seuraavasti:

kurssilainen1 = opiskelija.Opiskelija(nimi1, op_nro1)
# muita koodiriveja
kurssilainen2 = opiskelija.Opiskelija(nimi2, op_nro2)

Muuten pääohjelma säilyy samanlaisena kuin ennenkin. Esimerkiksi metodien kutsussa ei tarvita moduulin nimeä.

On myös toinen tapa kirjoittaa import-käsky pääohjelmatiedoston alkuun:

from opiskelija import *

Jos käytetään tätä tapaa, ei Opiskelija-olioita luodessa tarvitse antaa luokan sisältävän moduulin nimeä, vaan oliot luodaan samalla tavalla kuin jos luokan koodi olisi samassa moduulissa:

kurssilainen1 = Opiskelija(nimi1, op_nro1)
# muita koodiriveja
kurssilainen2 = Opiskelija(nimi2, op_nro2)

Merkkijonoesitys oliosta

Hyvin monessa ohjelmassa halutaan jossain vaiheessa tulostaa käsiteltävien olioiden kaikkien kenttien arvot. Tämä voidaan toki tehdä käyttämällä kenttien arvot palauttavia metodeita ja tulostamalla niiden palauttamat arvot, kuten edellisessä esimerkissä oli tehty. Se on kuitenkin aika työläs tapa erityisesti silloin, kun kenttiä on paljon. Tulostaminen voidaan tehdä helpommin, jos luokkaan kirjoitetaan erityinen metodi __str__. Metodi kirjoitetaan siten, että se palauttaa merkkijonon, joka sisältää käsiteltävän olion kuvauksen, esimerkiksi kenttien arvot tai muuta haluttua tietoa oliosta. Toisin sanoen metodi tekee ja palauttaa oliosta merkkijonoesityksen.

Luokkaan Opiskelija voidaan kirjoittaa (luokan sisään) seuraava __str__-metodi:

# Metodi palauttaa merkkijonoesityksen opiskelijan tiedoista.

def __str__(self):
    mjono = self.__nimi + ", " + self.__opiskelijanumero + \
            ", tenttiarvosana: " + str(self.__tenttiarvosana) + \
            ", harjoitusarvosana: " + str(self.__harjoitusarvosana)
    return mjono

Jos metodi on kirjoitettu, luokan olioiden tiedot voidaan tulostaa (yleensä luokan ulkopuolella, esimerkiksi pääohjelmassa) antamalla print-käskyssä vain sen muuttujan nimi, joka viittaa tulostettavaan olioon, esimerkiksi

print(kurssilainen1)

Näin edellisen esimerkkipääohjelman lopussa oleva osa, joka tulostaa luotujen Opiskelija-olioiden tiedot voidaan korvata seuraavalla, lyhyemmällä ja yksinkertaisemmalla ohjelmakoodilla:

print("1. opiskelijan tiedot:")
print(kurssilainen1)
print("Kurssiarvosana:", kurssilainen1.laske_kokonaisarvosana())

print("2. opiskelijan tiedot:")
print(kurssilainen2)
print("Kurssiarvosana:", kurssilainen2.laske_kokonaisarvosana())

Kokonaisarvosanojen tulostaminen on tässäkin koodissa erikseen, koska __str__-metodin palauttama merkkijono ei sisällä kokonaisarvosanaa.

Jos pääohjelman loppu muutetaan tällaiseksi, niin edellisen esimerkkiajon suorituksen loppu näyttää seuraavalta:

1. opiskelijan tiedot:
Minni Hiiri, 11223F, tenttiarvosana: 3, harjoitusarvosana: 5
Kurssiarvosana: 4
2. opiskelijan tiedot:
Hessu Hopo, 33441C, tenttiarvosana: 1, harjoitusarvosana: 2
Kurssiarvosana: 2

Tulostus on vähän erilainen kuin edellisessä esimerkkiajossa, koska __str__-metodin palauttama merkkijono poikkeaa vähän edellisen esimerkkiajon pääohjelman tulostuksista.