Kapselointi
Olio-ohjelmoinnissa asiakkaalla tarkoitetaan luokkaa tai siitä muodostettuja olioita käyttävää ohjelmaa. Luokka tarjoaa asiakkaalle palveluja, joiden avulla asiakas voi käyttää olioita. Päämääränä on, että
- asiakkaan kannalta luokan ja olioiden käyttö on mahdollisimman yksinkertaista ja
- olion sisäinen eheys säilyy joka tilanteessa.
Sisäisellä eheydellä tarkoitetaan, että olion tila (eli käytännössä olion attribuuttien arvot) pysyy koko ajan hyväksyttävänä. Virheellinen tila olisi esimerkiksi sellainen, jossa päivämäärää esittävälle oliolle kuukauden numero on 13 tai opiskelijaa esittävällä oliolla opintopistemäärä on negatiivinen luku.
Tarkastellaan esimerkkinä luokkaa Opiskelija:
class Opiskelija:
def __init__(self, nimi: str, opiskelijanumero: str):
self.nimi = nimi
self.opiskelijanumero = opiskelijanumero
self.opintopisteet = 0
def lisaa_suoritus(self, opintopisteet):
if opintopisteet > 0:
self.opintopisteet += opintopisteet
Opiskelija
-olio tarjoaa asiakkaalle metodin lisaa_suoritus
, jolla opintopisteitä voidaan lisätä. Metodi varmistaa, että lisättävä opintopisteiden määrä on positiivinen. Esimerkiksi seuraava koodi lisää kolme suoritusta:
oskari = Opiskelija("Oskari Opiskelija", "12345")
oskari.lisaa_suoritus(5)
oskari.lisaa_suoritus(5)
oskari.lisaa_suoritus(10)
print("Opintopisteet:", oskari.opintopisteet)
Opintopisteet: 20
Asiakas pystyy kuitenkin muuttamaan opintopistemäärää myös suoraan viittaamalla attribuuttiin opintopisteet
. Näin olio voi päätyä virheelliseen tilaan, jossa se ei ole enää sisäisesti eheä:
oskari = Opiskelija("Oskari Opiskelija", "12345")
oskari.opintopisteet = -100
print("Opintopisteet:", oskari.opintopisteet)
Opintopisteet: -100
Kapselointi
Luokka voi piilottaa attribuutit asiakkailta. Pythonissa tämä tapahtuu lisäämällä attribuuttimuuttujan nimen alkuun kaksi alaviivaa __
:
class Pankkikortti:
# Attribuutti numero on piilotettu, nimi on näkyvissä
def __init__(self, numero: str, nimi: str):
self.__numero = numero
self.nimi = nimi
Piilotettu attribuutti ei näy asiakkaalle, vaan siihen viittaaminen aiheutta virheilmoituksen. Niinpä nimen voi tulostaa ja sitä voi muuttaa:
kortti = Pankkikortti("123456","Reijo Rahakas")
print(kortti.nimi)
kortti.nimi = "Reijo Rutiköyhä"
print(kortti.nimi)
Reijo Rahakas Reijo Rutiköyhä
Mutta jos kortin numeroa yritetään tulostaa, seuraa virheilmoitus:
kortti = Pankkikortti("123456","Reijo Rahakas")
print(kortti.__numero)
AttributeError: 'Pankkikortti' object has no attribute '__numero'
Tietojen piilottamista asiakkaalta kutsutaan kapseloinniksi. Nimensä mukaisesti attribuutti siis "suljetaan kapseliin" ja asiakkalle tarjotaan sopiva rajapinta, jonka kautta tietoa voi käsitellä.
Laajennetaan pankkikorttiesimerkkiä niin, että kortilla on piilotettu attribuutti saldo ja tämän käsittelyyn tarkoitetut julkiset metodit, joiden avulla asiakas voi hallita saldoa:
class Pankkikortti:
def __init__(self, numero: str, nimi: str, saldo: float):
self.__numero = numero
self.nimi = nimi
self.__saldo = saldo
def lisaa_rahaa(self, maara: float):
if maara > 0:
self.__saldo += maara
def kayta_rahaa(self, maara: float):
if maara > 0 and maara <= self.__saldo:
self.__saldo -= maara
def hae_saldo(self):
return self.__saldo
kortti = Pankkikortti("123456", "Reijo Rahakas", 5000)
print(kortti.hae_saldo())
kortti.lisaa_rahaa(100)
print(kortti.hae_saldo())
kortti.kayta_rahaa(500)
print(kortti.hae_saldo())
# Tämä ei onnistu, koska saldo ei riitä
kortti.kayta_rahaa(10000)
print(kortti.hae_saldo())
5000 5100 4600 4600
Saldoa ei voi suoraan muuttaa, koska attribuutti on piilotettu, mutta sitä voi muuttaa metodeilla lisaa_rahaa
ja kayta_rahaa
ja sen voi hakea metodilla hae_saldo
. Metodeihin voidaan sijoittaa sopivia tarkastuksia, joilla varmistetaan, että olion sisäinen eheys säilyy: esimerkiksi rahaa ei voi käyttää enempää kuin kortilla on saldoa jäljellä.
Asetus- ja havainnointimetodit
Python tarjoaa myös suoraviivaisemman syntaksin attribuuttien havainnoimiselle ja asettamiselle. Tarkastellaan ensin esimerkkinä yksinkertaista luokkaa Lompakko
, jossa ainoa attribuutti rahaa
on suojattu asiakkailta:
class Lompakko:
def __init__(self):
self.__rahaa = 0
Luokkaan voidaan lisätä havainnointi- ja asetusmetodit, joilla asiakas voi hallita rahamäärää:
class Lompakko:
def __init__(self):
self.__rahaa = 0
# Havainnointimetodi
@property
def rahaa(self):
return self.__rahaa
# Asetusmetodi
@rahaa.setter
def rahaa(self, rahaa):
if rahaa >= 0:
self.__rahaa = rahaa
Luokalle siis määritellään ensin havainnointimetodi, joka palauttaa rahamäärän, ja sitten asetusmetodi, joka asettaa rahamäärän ja varmistaa, että uusi rahamäärä ei ole negatiivinen.
Kutsuminen tapahtuu esimerkiksi näin:
lompsa = Lompakko()
print(lompsa.rahaa)
lompsa.rahaa = 50
print(lompsa.rahaa)
lompsa.rahaa = -30
print(lompsa.rahaa)
0 50 50
Asiakkaan kannalta metodien kutsuminen muistuttaa attribuuttien kutsumista, koska kutsussa ei käytetä sulkuja vaan voi kirjoittaa esimerkiksilompsa.rahaa = 50
. Tarkoituksena onkin piilottaa (eli kapseloida) sisäinen toteutus ja tarjota asiakkaalle vaivaton tapa muokata olion tietoja.
Edellisessä esimerkissä on kuitenkin yksi pieni vika: asiakas ei saa mitään viestiä siitä, että negatiivisen rahasumman asettaminen ei toimi. Kun arvo on selvästi virheellinen, hyvä tapa viestiä tästä on heittää poikkeus. Tässä tapauksessa oikea poikkeus voisi olla ValueError
, joka kertoo että arvo on väärä.
Korjattu versio luokasta ja testikoodi:
class Lompakko:
def __init__(self):
self.__rahaa = 0
# Havainnointimetodi
@property
def rahaa(self):
return self.__rahaa
# Asetusmetodi
@rahaa.setter
def rahaa(self, rahaa):
if rahaa >= 0:
self.__rahaa = rahaa
else:
raise ValueError("Rahasumma ei saa olla negatiivinen")
lompsa.rahaa = -30
print(lompsa.rahaa)
ValueError: Rahasumma ei saa olla negatiivinen
Huomaa, että havainnointimetodi eli @property
-dekoraattori pitää esitellä luokassa ennen asetusmetodia, muuten seuraa virhe. Tämä johtuu siitä, että @property
-dekoraattori määrittelee käytettävän "asetusattribuutin" nimen (edellisessä esimerkiksi rahaa
), ja asetusmetodi .setter
liittää siihen uuden toiminnallisuuden.
Katsotaan vielä esimerkki luokasta, jolla on kaksi suojattua attribuuttia ja molemmille havainnointi- ja asetusmetodit:
class Pelaaja:
def __init__(self, nimi: str, pelinumero: int):
self.__nimi = nimi
self.__pelinumero = pelinumero
@property
def nimi(self):
return self.__nimi
@nimi.setter
def nimi(self, nimi: str):
if nimi != "":
self.__nimi = nimi
else:
raise ValueError("Nimi ei voi olla tyhjä")
@property
def pelinumero(self):
return self.__pelinumero
@pelinumero.setter
def pelinumero(self, pelinumero: int):
if pelinumero > 0:
self.__pelinumero = pelinumero
else:
raise ValueError("Pelinumeron täytyy olla positiviinen kokonaisluku")
pelaaja = Pelaaja("Pekka Palloilija", 10)
print(pelaaja.nimi)
print(pelaaja.pelinumero)
pelaaja.nimi = "Paula Palloilija"
pelaaja.pelinumero = 11
print(pelaaja.nimi)
print(pelaaja.pelinumero)
Pekka Palloilija 10 Paula Palloilija 11
Kolmantena esimerkkinä tarkastellaan luokkaa, joka mallintaa päiväkirjaa. Huomaa, että omistajalla on asetus- ja havainnointimetodit, mutta merkintöjen lisäys on toteutettu "perinteisillä" metodeilla. Tämä siksi, että asiakkalle ei ole haluttu tarjota suoraan pääsyä tietorakenteeseen, johon merkinnät tallennetaan. Kapseloinnista on tässä sekin hyöty, että sisäistä toteutusta voidaan myöhemmin muuttaa (esim. vaihtamalla lista vaikka sanakirjaksi) ilman, että asiakkaan täytyy muuttaa omaa koodiaan.
class Paivakirja:
def __init__(self, omistaja: str):
self.__omistaja = omistaja
self.__merkinnat = []
@property
def omistaja(self):
return self.__omistaja
@omistaja.setter
def omistaja(self, omistaja):
if omistaja != "":
self.__omistaja = omistaja
else:
raise ValueError("Omistaja ei voi olla tyhjä")
def lisaa_merkinta(self, merkinta: str):
self.__merkinnat.append(merkinta)
def tulosta(self):
print("Yhteensä", len(self.__merkinnat), "merkintää")
for merkinta in self.__merkinnat:
print("- " + merkinta)
paivakirja = Paivakirja("Pekka")
paivakirja.lisaa_merkinta("Tänään söin puuroa")
paivakirja.lisaa_merkinta("Tänään opettelin olio-ohjelmointia")
paivakirja.lisaa_merkinta("Tänään menin ajoissa nukkumaan")
paivakirja.tulosta()
Yhteensä 3 merkintää
- Tänään söin puuroa
- Tänään opettelin olio-ohjelmointia
- Tänään menin ajoissa nukkumaan