Olio-ohjelmoinnin tekniikoita
Luokka voi palauttaa metodista myös sen itsensä tyyppisen olion. Luokan Tuote
metodi alennustuote
palauttaa uuden tuotteen, jolla on sama nimi kuin nykyisellä tuotteella, mutta 25% halvempi hinta:
class Tuote:
def __init__(self, nimi: str, hinta: float):
self.__nimi = nimi
self.__hinta = hinta
def __str__(self):
return f"{self.__nimi} (hinta {self.__hinta})"
def alennustuote(self):
alennettu = Tuote(self.__nimi, self.__hinta * 0.75)
return alennettu
omena1 = Tuote("Omena", 2.99)
omena2 = omena1.alennustuote()
print(omena1)
print(omena2)
Omena (hinta 2.99) Omena (hinta 2.2425)
Kerrataan vielä muuttujan self
merkitys: luokan sisällä se viittaa nykyiseen olioon. Tyypillinen tapa käyttää muuttujaa onkin viitata olion omiin piirteisiin, esimerkiksi attribuuttien arvoihin. Muuttujaa voidaan käyttää myös palauttamaan koko olio (vaikka tälle onkin selvästi harvemmin tarvetta). Esimerkkiluokan Tuote
metodi halvempi
osaa palauttaa halvemman tuotteen, kun sille annetaan parametriksi toinen Tuote
-luokan olio:
class Tuote:
def __init__(self, nimi: str, hinta: float):
self.__nimi = nimi
self.__hinta = hinta
def __str__(self):
return f"{self.__nimi} (hinta {self.__hinta})"
@property
def hinta(self):
return self.__hinta
def halvempi(self, tuote):
if self.__hinta < tuote.hinta:
return self
else:
return tuote
omena = Tuote("Omena", 2.99)
appelsiini = Tuote("Appelsiini", 3.95)
banaani = Tuote("Banaani", 5.25)
print(appelsiini.halvempi(omena))
print(appelsiini.halvempi(banaani))
Omena (2.99) Appelsiini (3.95)
Esimerkin vertailun toteutus vaikuttaa kuitenkin melko kömpelöltä - paljon parempi olisi, jos voisimme vertailla Tuote
-olioita suoraan Pythonin vertailuoperaattoreilla.
Operaattorien ylikuormitus
Pythonin lasku- ja vertailuoperaattorien käyttö omien olioiden kanssa on onneksi mahdollista. Tähän käytetään tekniikkaa, jonka nimi on operaattorien ylikuormitus. Kun halutaan, että tietty operaattori toimii myös omasta luokasta muodostettujen olioiden kanssa, luokkaan kirjoitetaan vastaava metodi joka palauttaa oikean lopputuloksen. Periaate on vastaava kuin metodin __str__
kanssa: Python osaa käyttää tietyllä tapaa nimettyjä metodeja tietyissä operaatioissa.
Tarkastellaan ensin esimerkkiä, jossa Tuote
-luokkaan on toteutettu metodi __gt__
(lyhenne sanoista greater than) joka toteuttaa suurempi kuin -operaattorin. Tarkemmin sanottuna metodi palauttaa arvon True
, jos nykyinen olio on suurempi kuin parametrina annettu olio.
class Tuote:
def __init__(self, nimi: str, hinta: float):
self.__nimi = nimi
self.__hinta = hinta
def __str__(self):
return f"{self.__nimi} (hinta {self.__hinta})"
@property
def hinta(self):
return self.__hinta
def __gt__(self, toinen_tuote):
return self.hinta > toinen_tuote.hinta
Metodi __gt__
palauttaa arvon True
, jos nykyisen tuotteen hinta on suurempi kuin parametrina annetun tuotteen, ja muuten arvon False
.
Nyt luokan olioita voidaan vertailla käyttäen >
-operaattoria samalla tavalla kuin vaikkapa kokonaislukuja:
appelsiini = Tuote("Appelsiini", 4.90)
omena = Tuote("Omena", 3.95)
if appelsiini > omena:
print("Appelsiini on suurempi")
else:
print("Omena on suurempi")
Appelsiini on suurempi
Olioiden suuruusluokan vertailua toteuttaessa täytyy päättää, millä perusteella suuruusjärjestys määritetään. Voisimme myös haluta, että tuotteet järjestetään hinnan sijasta nimen mukaiseen aakkosjärjestykseen. Tällöin omena olisikin appelsiinia "suurempi":
class Tuote:
def __init__(self, nimi: str, hinta: float):
self.__nimi = nimi
self.__hinta = hinta
def __str__(self):
return f"{self.__nimi} (hinta {self.__hinta})"
@property
def hinta(self):
return self.__hinta
@property
def nimi(self):
return self.__nimi
def __gt__(self, toinen_tuote):
return self.nimi > toinen_tuote.nimi
appelsiini = Tuote("Appelsiini", 4.90)
omena = Tuote("Omena", 3.95)
if appelsiini > omena:
print("Appelsiini on suurempi")
else:
print("Omena on suurempi")
Omena on suurempi
Lisää operaattoreita
Tavalliset vertailuoperaattorit ja näitä vastaavat metodit on esitetty seuraavassa taulukossa:
Operaattori | Merkitys perinteisesti | Metodin nimi |
---|---|---|
< | Pienempi kuin | __lt__(self, toinen) |
> | Suurempi kuin | __gt__(self, toinen) |
== | Yhtä suuri kuin | __eq__(self, toinen) |
!= | Eri suuri kuin | __ne__(self, toinen) |
<= | Pienempi tai yhtäsuuri kuin | __le__(self, toinen) |
>= | Suurempi tai yhtäsuuri kuin | __ge__(self, toinen) |
Lisäksi luokissa voidaan toteuttaa tiettyjä muita operaattoreita, esimerkiksi:
Operaattori | Merkitys perinteisesti | Metodin nimi |
---|---|---|
+ | Yhdistäminen | __add__(self, toinen) |
- | Vähentäminen | __sub__(self, toinen) |
* | Monistaminen | __mul__(self, toinen) |
/ | Jakaminen | __truediv__(self, toinen) |
// | Kokonaisjakaminen | __floordiv__(self, toinen) |
Lisää operaattoreita ja metodien nimien vastineita löydät helposti Googlella.
Huomaa, että vain hyvin harvoin on tarvetta toteuttaa kaikkia operaatioita omassa luokassa. Esimerkiksi jakaminen on operaatio, jolle on hankalaa keksiä luontevaa käyttöä useimmissa luokissa (mitä tulee, kun jaetaan opiskelija kolmella saati toisella opiskelijalla?). Tiettyjen operaattoreiden toteuttamisesta voi kuitenkin olla hyötyä, mikäli vastaavat operaatiot ovat loogisia luokalle.
Tarkastellaan esimerkkinä luokkaa joka mallintaa yhtä muistiinpanoa. Kahden muistiinpanon yhdistäminen +
-operaattorilla tuottaa uuden, yhdistetyn muistiinpanon, kun on toteutettu metodi __add__
:
from datetime import datetime
class Muistiinpano:
def __init__(self, pvm: datetime, merkinta: str):
self.pvm = pvm
self.merkinta = merkinta
def __str__(self):
return f"{self.pvm}: {self.merkinta}"
def __add__(self, toinen):
# Uuden muistiinpanon ajaksi nykyinen aika
uusi_muistiinpano = Muistiinpano(datetime.now(), "")
uusi_muistiinpano.merkinta = self.merkinta + " ja " + toinen.merkinta
return uusi_muistiinpano
merkinta1 = Muistiinpano(datetime(2016, 12, 17), "Muista ostaa lahjoja")
merkinta2 = Muistiinpano(datetime(2016, 12, 23), "Muista hakea kuusi")
# Nyt voidaan yhdistää plussalla - tämä kutsuu metodia __add__ luokassa Muistiipano
molemmat = merkinta1 + merkinta2
print(molemmat)
2020-09-09 14:13:02.163170: Muista ostaa lahjoja ja Muista hakea kuusi
Olion esitys merkkijonona
Olemme toteuttaneet luokkiin usein metodin __str__
, joka antaa merkkijonoesityksen olion sisällöstä. Toinen melko samanlainen metodi on __repr__
, joka antaa teknisen esityksen olion sisällöstä. Usein metodi __repr__
toteutetaan niin, että se antaa koodin, joka muodostaa olion.
Funktio repr
antaa olion teknisen merkkijonoesityksen, ja lisäksi tätä esitystä käytetään, jos oliossa ei ole määritelty __str__
-metodia. Seuraava luokka esittelee asiaa:
class Henkilo:
def __init__(self, nimi: str, ika: int):
self.nimi = nimi
self.ika = ika
def __repr__(self):
return f"Henkilo({repr(self.nimi)}, {self.ika})"
henkilo1 = Henkilo("Anna", 25)
henkilo2 = Henkilo("Pekka", 99)
print(henkilo1)
print(henkilo2)
Henkilo('Anna', 25) Henkilo('Pekka', 99)
Huomaa, että metodissa __repr__
haetaan nimen tekninen esitys metodilla repr
, jolloin tässä tapauksessa nimen ympärille tulee '
-merkit.
Seuraavassa luokassa on toteutettu sekä metodi __repr__
että __str__
:
class Henkilo:
def __init__(self, nimi: str, ika: int):
self.nimi = nimi
self.ika = ika
def __repr__(self):
return f"Henkilo({repr(self.nimi)}, {self.ika})"
def __str__(self):
return f"{self.nimi} ({self.ika} vuotta)"
henkilo = Henkilo("Anna", 25)
print(henkilo)
print(repr(henkilo))
Anna (25 vuotta) Henkilo('Anna', 25)
Kun tietorakenteessa (kuten listassa) on olioita, Python käyttää vähän epäloogisesti metodia __repr__
olioiden merkkijonoesityksen muodostamiseen, kun lista tulostetaan:
henkilot = []
henkilot.append(Henkilo("Anna", 25))
henkilot.append(Henkilo("Pekka", 99))
henkilot.append(Henkilo("Maija", 55))
print(henkilot)
[Henkilo('Anna', 25), Henkilo('Pekka', 99), Henkilo('Maija', 55)]
Iteraattorit
Olemme aikaisemmin käyttäneet for-lausetta erilaisten tietorakenteiden ja tiedostojen iterointiin eli läpikäyntiin. Tyypillinen tapaus olisi vaikkapa seuraavanlainen funktio:
def laske_positiiviset(lista: list):
n = 0
for alkio in lista:
if alkio > 0:
n += 1
return n
Funktio käy läpi listan alkio kerrallaan ja laskee positiivisten alkioiden määärän.
Iterointi on mahdollista toteuttaa myös omiin luokkiin. Hyödyllistä tämä on silloin, kun luokasta muodostetut oliot tallentavat kokoelman alkioita. Esimerkiksi aikaisemmin kirjoitettiin luokka, joka mallintaa kirjahyllyä – olisi näppärä, jos kaikki kirjahyllyn kirjat voisi käydä läpi yhdessä silmukassa. Samalla tavalla opiskelijarekisterin kaikkien opiskelijoiden läpikäynti for-lauseella olisi kätevää.
Iterointi mahdollistuu toteuttamalla luokkaan iteraattorimetodit __iter__
ja __next__
. Käsitellään metodien toimintaa tarkemmin, kun on ensin tarkasteltu esimerkkinä kirjahyllyluokkaa, joka mahdollistaa kirjojen läpikäynnin:
class Kirja:
def __init__(self, nimi: str, kirjailija: str, sivuja: int):
self.nimi = nimi
self.kirjailija = kirjailija
self.sivuja = sivuja
class Kirjahylly:
def __init__(self):
self._kirjat = []
def lisaa_kirja(self, kirja: Kirja):
self._kirjat.append(kirja)
# Iteraattorin alustusmetodi
# Tässä tulee alustaa iteroinnissa käytettävä(t) muuttuja(t)
def __iter__(self):
self.n = 0
# Metodi palauttaa viittauksen olioon itseensä, koska
# iteraattori on toteutettu samassa luokassa
return self
# Metodi palauttaa seuraavan alkion
# Jos ei ole enempää alkioita, heitetään tapahtuma
# StopIteration
def __next__(self):
if self.n < len(self._kirjat):
# Poimitaan listasta nykyinen
kirja = self._kirjat[self.n]
# Kasvatetaan laskuria yhdellä
self.n += 1
# ...ja palautetaan
return kirja
else:
# Ei enempää kirjoja
raise StopIteration
Metodissa __iter__
siis alustetaan iteroinnissa tarvittava muuttuja tai muuttujat - tässä tapauksessa riittää, että meillä on laskuri joka osoittaa listan nykyiseen alkioon. Lisäksi tarvitaan metodi __next__
, joka palauttaa seuraavan alkion. Esimerkkitapauksessa palautetaan listasta alkio muuttujan n
kohdalta ja kasvatetaan muuttujan arvoa yhdellä. Jos listassa ei ole enempää alkiota, "nostetaan" poikkeus StopIteration
, joka kertoo iteroijalle (esim. for-silmukalle), että kaikki alkiot on käyty läpi.
Nyt voidaan käydä kirjahyllyn kirjat läpi esimerkiksi for-silmukassa näppärästi:
if __name__ == "__main__":
k1 = Kirja("Elämäni Pythoniassa", "Pekka Python", 123)
k2 = Kirja("Vanhus ja Java", "Ernest Hemingjava", 204)
k3 = Kirja("C-itsemän veljestä", "Keijo Koodari", 997)
hylly = Kirjahylly()
hylly.lisaa_kirja(k1)
hylly.lisaa_kirja(k2)
hylly.lisaa_kirja(k3)
# Tulostetaan kaikkien kirjojen nimet
for kirja in hylly:
print(kirja.nimi)
Elämäni Pythoniassa Vanhus ja Java C-itsemän veljestä