Metodit omissa luokissa
Vain attribuutteja sisältävät luokat eivät käytännössä eroa juurikaan sanakirjoista. Seuraavassa esimerkissä on esitetty pankkitiliä mallintava rakenne sekä oman luokan että sanakirjan avulla toteutettuna:
# Esimerkki omaa luokkaa käyttäen
class Pankkitili:
def __init__(self, tilinumero: str, omistaja: str, saldo: float, vuosikorko: float):
self.tilinumero = tilinumero
self.omistaja = omistaja
self.saldo = saldo
self.vuosikorko = vuosikorko
pekan_tili = Pankkitili("12345-678", "Pekka Python", 1500.0, 0.015)
# Esimerkki sanakirjaa käyttäen
pekan_tili = {"tilinumero": "12345-678", "omistaja": "Pekka Python", "saldo": 1500.0, "vuosikorko": 0.0}
Sanakirjaa käyttäen rakenteen toteutus on huomattavasti suoraviivaisempi ja koodi on lyhyempi. Luokan hyötynä tässä tapauksessa on, että se määrittelee rakenteen "tiukemmin", jolloin kaikki luokasta muodostetut oliot ovat rakenteeltaan samanlaisia. Luokka on lisäksi nimetty: oliota muodostaessa viitataan Pankkitili
-luokkaan ja olion tyyppi on Pankkitili
eikä sanakirja.
Luokilla on lisäksi etuna, että niihin voidaan lisätä attribuuttien lisäksi myös toiminnallisuutta. Yksi olio-ohjelmoinnin periaatteista onkin, että olioon on yhdistetty sekä tallennettavat tiedot että operaatiot, joilla tietoa voidaan käsitellä.
Metodit luokissa
Metodi tarkoittaa luokkaan sidottua aliohjelmaa. Yleensä metodin toiminta kohdistuu vain yhteen olioon. Metodi kirjoitetaan luokan sisälle, ja se voi käsitellä attribuutteja kuten mitä tahansa muuttujia.
Katsotaan esimerkkinä Pankkitili
-luokan metodia, joka lisää koron pankkitilille:
class Pankkitili:
def __init__(self, tilinumero: str, omistaja: str, saldo: float, vuosikorko: float):
self.tilinumero = tilinumero
self.omistaja = omistaja
self.saldo = saldo
self.vuosikorko = vuosikorko
# Metodi lisää koron tilin saldoon
def lisaa_korko(self):
self.saldo += self.saldo * self.vuosikorko
pekan_tili = Pankkitili("12345-678", "Pekka Python", 1500.0, 0.015)
pekan_tili.lisaa_korko()
print(pekan_tili.saldo)
1522.5
Metodi lisaa_korko
kertoo olion saldon vuosikorkoprosentilla ja lisää tuloksen nykyiseen saldoon. Metodin toiminta kohdistuu siihen olioon, jonka kautta sitä kutsutaan.
Katsotaan vielä toinen esimerkki, jossa luokasta on muodostettu useampi olio:
# Luokka Pankkitili on määritelty edellisessä esimerkissä
pekan_tili = Pankkitili("12345-678", "Pekka Python", 1500.0, 0.015)
pirjon_tili = Pankkitili("99999-999", "Pirjo Pythonen", 1500.0, 0.05)
paulin_tili = Pankkitili("1111-222", "Pauli Paulinen", 1500.0, 0.001)
# Lisätään korko Pekalle ja Pirjolle, mutta ei Paulille
pekan_tili.lisaa_korko()
pirjon_tili.lisaa_korko()
# Tulostetaan kaikki
print(pekan_tili.saldo)
print(pirjon_tili.saldo)
print(paulin_tili.saldo)
1522.5 1575.0 1500.0
Korko lisätään vain siihen tiliin, jonka kautta metodia kutsutaan. Esimerkistä huomataan, että Pekalle ja Pirjolle lisätään eri korkoprosentit ja Paulin tilin saldo ei muutu ollenkaan, koska olion paulin_tili
kautta ei kutsuta metodia lisaa_korko
.
Kapselointi
Olio-ohjelmoinnin yhteydessä puhutaan usein olioiden asiakkaista. Asiakkaalla (client) tarkoitetaan koodin osaa, joka muodostaa olion ja käyttää sen palveluita kutsumalla metodeita. Kun olion tietosisältöä käsitellään vain olion tarjoamien metodien avulla, voidaan varmistua siitä, että olion sisäinen eheys säilyy. Käytännössä tämä tarkoittaa esimerkiksi sitä, että Pankkitili
-luokassa tarjotaan metodi, jolla tililtä nostetaan rahaa, sen sijaan, että asiakas käsittelisi suoraan attribuuttia saldo
. Tässä metodissa voidaan sitten esimerkiksi varmistaa, ettei tililtä nosteta katetta enempää rahaa.
Esimerkiksi:
class Pankkitili:
def __init__(self, tilinumero: str, omistaja: str, saldo: float, vuosikorko: float):
self.tilinumero = tilinumero
self.omistaja = omistaja
self.saldo = saldo
self.vuosikorko = vuosikorko
# Metodi lisää koron tilin saldoon
def lisaa_korko(self):
self.saldo += self.saldo * self.vuosikorko
# Metodilla "nostetaan" tililtä rahaa
# Metodi palauttaa true, jos nosto onnistuu, muuten False
def nosto(self, nostosumma: float):
if nostosumma <= self.saldo:
self.saldo -= nostosumma
return True
return False
pekan_tili = Pankkitili("12345-678", "Pekka Python", 1500.0, 0.015)
if pekan_tili.nosto(1000):
print("Nosto onnistui, tilin saldo on nyt", pekan_tili.saldo)
else:
print("Nosto ei onnistunut, rahaa ei ole tarpeeksi.")
# Yritetään uudestaan
if pekan_tili.nosto(1000):
print("Nosto onnistui, tilin saldo on nyt", pekan_tili.saldo)
else:
print("Nosto ei onnistunut, rahaa ei ole tarpeeksi.")
Nosto onnistui, tilin saldo on nyt 500.0 Nosto ei onnistunut, rahaa ei ole tarpeeksi.
Olion sisäisen eheyden säilyttämistä ja sopivien metodien tarjoamista asiakkaalle kutsutaan kapseloinniksi. Tämä tarkoittaa, että olion toteutus piilotetaan asiakkaalta ja olio tarjoaa ulkopuolelle metodit, joiden avulla tietoja voi käsitellä.
Pelkkä metodin lisäys ei kuitenkaan piilota attribuuttia: vaikka luokkaan Pankkitili
onkin lisätty metodi nosto
rahan nostamiseksi, asiakas voi edelleen muokata saldo
-attribuutin arvoa suoraan:
pekan_tili = Pankkitili("12345-678", "Pekka Python", 1500.0, 0.015)
# Yritetään nostaa 2000
if pekan_tili.nosto(2000):
print("Nosto onnistui, tilin saldo on nyt", pekan_tili.saldo)
else:
print("Nosto ei onnistunut, rahaa ei ole tarpeeksi.")
# Nostetaan "väkisin" 2000
pekan_tili.saldo -= 2000
print("Saldo nyt:", pekan_tili.saldo)
Ongelma voidaan ainakin osittain ratkaista piilottamalla attribuutit asiakkaalta. Käytännön toteutukseen palataan tarkemmin ensi viikolla.
Tarkastellaan vielä esimerkkiä luokasta, joka mallintaa pelaajan ennätystulosta. Luokkaan on kirjoitettu erilliset metodit, joilla voidaan tarkastaa, ovatko annetut parametrit sopivia. Metodeja kutsutaan heti konstruktorissa. Näin varmistetaan luotavan olion sisäinen eheys.
from datetime import date
class Ennatystulos:
def __init__(self, pelaaja: str, paiva: int, kuukausi: int, vuosi: int, pisteet: int):
# Oletusarvot
self.pelaaja = ""
self.paivamaara = date(1900, 1, 1)
self.pisteet = 0
if self.nimi_ok(pelaaja):
self.pelaaja = pelaaja
if self.pvm_ok(paiva, kuukausi, vuosi):
self.paivamaara = date(vuosi, kuukausi, paiva)
if self.pisteet_ok(pisteet):
self.pisteet = pisteet
# Apumetodit, joilla tarkistetaan ovatko syötteet ok
def nimi_ok(self, nimi: str):
return len(nimi) >= 2 # Nimessä vähintään kaksi merkkiä
def pvm_ok(self, paiva, kuukausi, vuosi):
try:
date(vuosi, kuukausi, paiva)
return True
except:
# Poikkeus, jos yritetään muodostaa epäkelpo päivämäärä
return False
def pisteet_ok(self, pisteet):
return pisteet >= 0
if __name__ == "__main__":
tulos1 = Ennatystulos("Pekka", 1, 11, 2020, 235)
print(tulos1.pisteet)
print(tulos1.pelaaja)
print(tulos1.paivamaara)
# Epäkelpo arvo päivämäärälle
tulos2 = Ennatystulos("Piia", 4, 13, 2019, 4555)
print(tulos2.pisteet)
print(tulos2.pelaaja)
print(tulos2.paivamaara) # Tulostaa oletusarvon 1900-01-01
235 Pekka 2020-11-01 4555 Piia 1900-01-01
Esimerkistä huomataan, että myös olion omiin metodeihin pitää viitata self
-määreen avulla, kun niitä kutsutaan konstruktorista. Luokkiin voidaan kirjoittaa myös staattisia metodeita eli metodeja, joita voidaan kutsua ilman, että luokasta muodostetaan oliota. Tähän palataan kuitenkin tarkemmin ensi viikolla.
Määrettä self
käytetään kuitenkin vain silloin, kun viitataan olion piirteisiin (eli metodeihin tai olion attribuutteihin). Olion metodeissa voidaan käyttää myös paikallisia muuttujia. Tämä on suositeltavaa, jos muuttujaan ei ole tarvetta viitata metodin ulkopuolella.
Paikallinen muuttuja määritellään ilman self
-määrettä - eli samoin kuin esimerkiksi kaikki muuttujat kurssin ensimmäisellä puoliskolla.
Esimerkiksi
class Bonuskortti:
def __init__(self, nimi: str, saldo: float):
self.nimi = nimi
self.saldo = saldo
def lisaa_bonus(self):
# Nyt muuttuja bonus on paikallinen muuttuja,
# eikä olion attribuutti - siihen siis ei voi
# viitata olion kautta
bonus = self.saldo * 0.25
self.saldo += bonus
def lisaa_superbonus(self):
# Myös muuttuja superbonus on paikallinen muuttuja
# Yleensä apumuuttujina käytetään paikallisia
# muuttujia, koska niihin ei ole tarvetta
# viitatata muissa metodeissa tai olion kautta
superbonus = self.saldo * 0.5
self.saldo += superbonus
def __str__(self):
return f"Bonuskortti(nimi={self.nimi}, saldo={self.saldo})"