Osa 9

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ä

  1. asiakkaan kannalta luokan ja olioiden käyttö on mahdollisimman yksinkertaista ja
  2. 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)
Esimerkkitulostus

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)
Esimerkkitulostus

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)
Esimerkkitulostus

Reijo Rahakas Reijo Rutiköyhä

Mutta jos kortin numeroa yritetään tulostaa, seuraa virheilmoitus:

kortti = Pankkikortti("123456","Reijo Rahakas")
print(kortti.__numero)
Esimerkkitulostus

AttributeError: 'Pankkikortti' object has no attribute '__numero'

Tietojen piilottamista asiakkaalta kutsutaan kapseloinniksi. Nimensä mukaisesti attribuutti siis "suljetaan kapseliin" ja asiakkaalle 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())
Esimerkkitulostus

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ä.

Loading

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)
Esimerkkitulostus

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)
Esimerkkitulostus

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.

Loading

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)
Esimerkkitulostus

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ä asiakkaalle 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()
Esimerkkitulostus

Yhteensä 3 merkintää

  • Tänään söin puuroa
  • Tänään opettelin olio-ohjelmointia
  • Tänään menin ajoissa nukkumaan
Loading
Seuraava osa: