Osa 10

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

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

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

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

Omena on suurempi

Lisää operaattoreita

Tavalliset vertailuoperaattorit ja näitä vastaavat metodit on esitetty seuraavassa taulukossa:

OperaattoriMerkitys perinteisestiMetodin 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:

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

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

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

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

[Henkilo('Anna', 25), Henkilo('Pekka', 99), Henkilo('Maija', 55)]

Loading
Loading

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

Elämäni Pythoniassa Vanhus ja Java C-itsemän veljestä

Loading