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
.