Kenttien yksityisyydestä

Tämän luvun esimerkeissä kenttien nimet ovat aina alkaneet kahdella alaviivalla. Tämä on sen vuoksi, että tällaisella nimeämiskäytännöllä Pythonissa estetään se, että luokan ulkopuolelta käytäisiin käsiksi suoraan kenttien arvoihin esimerkiksi seuraavaan tyyliin:

print(a_vektori.__x_kerroin)
a_vektori.__y_kerroin = 3.0

Tämä ei ole siis mahdollista, vaan kaikessa Tasovektori-luokan ulkopuolella olevassa koodissa kenttien arvoja on käsiteltevä luokassa määriteltyjen metodien kautta, esimerkiksi

print(a_vektori.kerro_x_kerroin())

Ensiksi voi tuntua siltä, että on turhan hankalaa käsitellä kenttiä vain luokan metodien avulla. Eikö olisi paljon helpompaa, jos kenttiä voisi käsitellä luokan ulkopuolellakin suoraan?

Kenttien suoran käytön sulkeminen luokan sisälle saa kuitenkin aikaan sen, että luokan sisäistä esitystä voidaan myöhemmin muuttaa tarvittaessa helposti ilman että luokkaa käyttäviä ohjelmia tarvitsee muuttaa.

Tarkastellaan esimerkkinä tilannetta, jossa on määritelty luokka yhden henkilön kuvaamiseen. Henkilöllä on ainakin kentät nimi ja ika. (Luokan olioilla voi olla monia muitakin kenttiä, mutta ne eivät ole oleellisia esiteltävän asian kannalta.) Luokan määrittely siis alkaa seuraavasti:

class Henkilo1:

    def __init__(self, nimi1):
        self.nimi = nimi1
        self.ika = 0

Luokkaa käytetään osana monia eri ohjelmia ohjelmassa käsiteltävien henkilöiden henkilötietojen esittämiseen. Esimerkiksi eräässä ohjelmassa voisi olla seuraavaa koodia (esitetyt käskyt ovat täysin mahdollisia, koska kenttiä nimi ja ika voi käsitellä vapaasti myös luokan ulkopuolella).

oppilas = Henkilo1("Matti")
oppilas.ika = 15
if oppilas.ika < 18:
    print("Oppilas on alaikainen")
else:
    print("Oppilas on taysi-ikainen")
oppilas.ika = 16
print("Oppilaan ika on", oppilas.ika)

Jossain vaiheessa eri ohjelmistojen ylläpitäjät tulevat siihen tulokseen, että on hankalaa esittää henkilöiden ikiä vuosina, koska ikää pitää päivittää joka vuosi. Jos ohjelmistossa pidetään yllä tietoa tuhansista henkilöistä, tarkoittaa tämä tuhansien henkilötietojen päivittämistä vuosittain.

Niinpä päätetään muuttaa luokkaa Henkilo1 siten, että henkilöstä esitetäänkin iän sijaan henkilön syntymävuosi. Luokan uuden määrittelyn alku olisi siis seuraava:

class Henkilo1:

    def __init__(self, nimi1):
        self.nimi = nimi1
        self.syntymavuosi = 0

Tutkitaan, mitä muutoksia tämä vaatii edellä esitettyyn ohjelman osaan, jossa luodaan Henkilo1-olio ja käsitellään tätä. Huomataan, että kaikki kohdat, joissa viitataan henkilön ika-kenttään, pitää muuttaa. Muutokset eivät rajoitu pelkästään tähän ohjelmaan. Jos luokkaa on käytetty apuna monissa muissakin ohjelmissa, pitää kaikkiin näihin ohjelmiin tehdä muutoksia.

Tarkastellaan sitten vaihtoehtoista tapaa. Määritellään luokka Henkilo2. Tämä luokka on muuten samanlainen kuin alkuperäinen luokka Henkilo1, mutta olion kentät on määritelty nyt niin, että niihin ei pääse käsiksi suoraan luokan ulkopuolelta, vaan luokassa on määritelty omat metodit, joiden avulla kenttiä käsitellään. Luokan määrittelyn alku olisi seuraava:

class Henkilo2:

    def __init__(self, nimi1):
        self.__nimi = nimi1
        self.__ika = 0


    def kerro_ika(self):
        return self.__ika


    def muuta_ika(self, uusi_ika):
        if 0 <= uusi_ika <= 150:
            self.__ika = uusi_ika

Luokkaa käytettäisiin osana erilaisia ohjelmia henkilötietojen kuvaamiseen, esimerkiksi seuraavasti:

oppilas = Henkilo2("Matti")
oppilas.muuta_ika(15)
if oppilas.kerro_ika() < 18:
    print("Oppilas on alaikainen")
else:
    print("Oppilas on taysi-ikainen")
print("Oppilaan ika on", oppilas.kerro_ika())

Tutkitaan, mitä tapahtuu, jos henkilöstä päätetäänkin tallentaa iän sijasta syntymävuosi. Luokkaa Henkilo2 ja siinä määriteltyjä metodeita pitää luonnollisesti muuttaa (nykyinen vuosi voitaisiin selvittää käytetyn vakion sijaan esimerkiksi tietokoneen kellon ajasta, mutta koska tätä ei ole kurssilla opetettu, on vuosi määritelty vakion avulla):

NYKYINEN_VUOSI = 2024

class Henkilo2:

    def __init__(self, nimi1):
        self.__nimi = nimi1
        self.__syntymavuosi = 0


    def kerro_ika(self):
        return NYKYINEN_VUOSI - self.__syntymavuosi


    def muuta_ika(self, uusi_ika):
        if 0 <= uusi_ika <= 150:
            self.__syntymavuosi = NYKYINEN_VUOSI - uusi_ika

Tarkastellaan sitten, mitä pitää muuttaa aikaisemmassa ohjelmassa, joka luo Henkilo2-olion ja käsittelee sitä. Havaitaan, että tähän ohjelmaan ei tarvitse tehdä lainkaan muutoksia. Vaikka luokan Henkilo2 kenttiä on muutettu, niin luokan metodien toiminta ei ole luokan ulkopuolelta katsottuna muuttunut. Metodeita kutsutaan edelleen samalla tavalla kuin aikaisemminkin ja ne paluttavat samanlaiset arvot kuin aikaisemminkin. Tämä on suuri etu, sillä Henkilo2-olioita käyttäviä ohjelmia saattaa olla lukuisia. Nyt niitä ei tarvitse muuttaa mitenkään, vaan pelkkä Henkilo2-luokan muuttaminen riittää.

Esimerkissä näkyy myös toinenkin etu siitä, että olion kenttiä käsitellään vain luokan metodien kautta: Kentän arvoa muuttavaan metodiin on helppo lisätä tarkistus siitä, että kentälle ei yritetä antaa jotain kelvotonta arvoa, esimerkiksi negatiivista tai liian suurta ikää. Jos kentän arvoa muutetaan suoraan, pitää tämä tarkistus kirjoittaa erikseen jokaiseen ohjelman kohtaan, jossa kentän arvoa muutetaan tai sitten otetaan riski siitä, että jollakin kentällä voi olla ohjelmassa järjettömiä arvoja.

Tarkasti ottaen Pythonissa kenttien nimien yksityisyys ei ole niin tärkeää kuin edellä on esitetty, koska Pythonissa luokkaa jälkikäteen muokatessa pystyy Pythonin valmiin property-funktion avulla määräämään, että kentän suoran käsittelyn sijasta käytetäänkin määrättyä metodia. Monesta muusta olio-ohjelmointikielestä, esimerkiksi Javasta, tämä mahdollisuus kuitenkin puuttuu. Edellä opetettu tapa kenttien käsittelyyn on sellainen, jota voi soveltaa muillakin olio-ohjelmointikielellä ohjelmoidessa.

Vaikka kenttien nimien aloittaminen kahdella alaviivalla saakin aikaan sen, että kenttää ei voi käsitellä suoraan luokan ulkopuolelta, voi luokan sisällä olevissa metodeissa käsitellä suoraan kaikkien saman luokan olioiden kenttiä. Esimerkiksi Tasovektori-luokan pistetulo-metodin ensimmäinen versio oli kirjoitettu seuraavasti:

def pistetulo(self, toinen_vektori):
    tulo = self.__x_kerroin * toinen_vektori.__x_kerroin + \
           self.__y_kerroin * toinen_vektori.__y_kerroin
    return tulo

Tässä metodi käsittelee suoraan olion self kenttien lisäksi parametrina saadun olion toinen_vektori kenttiä. Tämä on täysin mahdollista, koska myös parametrina saatu olio on Tasovektori-olio. Jos sen sijaan metodin parametrina olisi esimerkiksi Opiskelija-olio, ei Tasovektori-luokassa oleva metodi voisi käsitellä suoraan sen kenttiä __nimi, __opiskelijanumero, __harjoitusarvosana ja __tenttiarvosana.