Osa 9

Oliot ja viittaukset

Pythonissa kaikki arvot ovat olioita ja myös omista luokista luotuja olioita voi käsitellä kuin mitä tahansa muitakin olioita. Esimerkiksi olioita voidaan tallentaa listaan:

from datetime import date

class Kurssisuoritus:

    def __init__(self, kurssi: str, opintopisteet: int, suorituspvm: date):
        self.kurssi = kurssi
        self.opintopisteet = opintopisteet
        self.suorituspvm = suorituspvm


if __name__ == "__main__":
    # Luodaan pari kurssisuoritusta ja lisätään listaan
    suoritukset = []

    mat1 = Kurssisuoritus("Matematiikka 1", 5, date(2020, 3, 11))
    ohj1 = Kurssisuoritus("Ohjelmointi 1", 6, date(2019, 12, 17))

    suoritukset.append(mat1)
    suoritukset.append(ohj1)

    # Lisätään suoraan listaan muutama
    suoritukset.append(Kurssisuoritus("Fysiikka 2", 4, date(2019, 11, 10)))
    suoritukset.append(Kurssisuoritus("Ohjelmointi 2", 5, date(2020, 5, 19)))

    # Käydään läpi kaikki suoritukset, tulostetaan nimet ja lasketaan opintopisteet yhteen
    opintopisteet = 0
    for suoritus in suoritukset:
        print(suoritus.kurssi)
        opintopisteet += suoritus.opintopisteet

    print("Opintopisteitä yhteensä:", opintopisteet)
Esimerkkitulostus

Matematiikka 1 Ohjelmointi 1 Fysiikka 2 Ohjelmointi 2 Opintopisteitä yhteensä: 20

Loading
Loading

Listaan ei tarkkaan ottaen tallenneta olioita vaan viittauksia olioihin. Niinpä sama olio voi esiintyä listassa useaan kertaan ja samaan olioon voidaan viitata useaan kertaan listassa ja sen ulkopuolella. Esimerkiksi näin:

class Tuote:
    def __init__(self, nimi: int, yksikko: str):
        self.nimi = nimi
        self.yksikko = yksikko


if __name__ == "__main__":
    kauppalista = []
    maito = Tuote("Maito", "litra")

    kauppalista.append(maito)
    kauppalista.append(maito)
    kauppalista.append(Tuote("Kurkku", "kpl"))
9 1 1

Jos samaan olioon on useampi kuin yksi viittaus, on lopputuloksen kannalta yhdentekevää, mitä viittauksista käytetään:

class Koira:
    def __init__(self, nimi):
        self.nimi = nimi

    def __str__(self):
        return self.nimi

koirat = []
musti = Koira("Musti")
koirat.append(musti)
koirat.append(musti)
koirat.append(Koira("Musti"))

print("Koirat alussa:")
for koira in koirat:
    print(koira)

print("Kohdan 0 koira saa uuden nimen:")
koirat[0].nimi = "Rekku"
for koira in koirat:
    print(koira)

print("Kohdan 2 koira saa uuden nimen:")
koirat[2].nimi = "Fifi"
for koira in koirat:
    print(koira)
Esimerkkitulostus

Koirat alussa: Musti Musti Musti Kohdan 0 koira saa uuden nimen: Rekku Rekku Musti Kohdan 2 koira saa uuden nimen: Rekku Rekku Fifi

Listan kohdissa 0 ja 1 on viittaus samaan olioon, joten olion sisältöä voidaan muuttaa kumman tahansa viittauksen kautta. Listan kohdassa 2 on kuitenkin viittaus toiseen olioon, minkä vuoksi tämän olion muuttaminen ei muuta muita.

Operaattorilla is voidaan tutkia, onko kyseessä täysin sama olio, ja operaattorilla == voidaan tutkia, onko kyseessä saman sisältöinen olio. Seuraava koodi havainnollistaa asiaa:

lista1 = [1, 2, 3]
lista2 = [1, 2, 3]
lista3 = lista1

print(lista1 is lista2)
print(lista1 is lista3)
print(lista2 is lista3)

print()

print(lista1 == lista2)
print(lista1 == lista3)
print(lista2 == lista3)
Esimerkkitulostus

False True False

True True True

Omista luokista muodostettuja olioita voidaan myös tallentaa esimerkiksi sanakirjaan ja muihin tietorakenteisiin:

class Opiskelija:
    def __init__(self, nimi: str, op: int):
        self.nimi = nimi
        self.op = op

if __name__ == "__main__":
    # Käytetään avaimena opiskelijanumeroa ja arvona Opiskelija-oliota
    opiskelijat = {}
    opiskelijat["12345"] = Opiskelija("Olli Opiskelija", 10)
    opiskelijat["54321"] = Opiskelija("Outi Opiskelija", 67)

Visualisaattori osaa havainnollistaa nämäkin asiat hienosti:

9 1 2

Selfillä vai ilman?

Tässä vaiheessa kurssia self-määre saattaa vaikuttaa vielä hämärältä. Käytetään siis hetki sen pohtimiseen, milloin selfiä tulee käyttää, ja milloin sitä kannattaa olla käyttämättä.

Tarkastellaan esimerkkinä yksinkertaista luokkaa, jonka avulla joukosta sanoja on mahdollista muodostaa sanasto:

class Sanasto:
    def __init__(self):
        self.sanat = []

    def lisaa_sana(self, sana: str):
        if not sana in self.sanat:
            self.sanat.append(sana)

    def tulosta(self):
        for sana in sorted(self.sanat):
            print(sana)

sanasto = Sanasto()
sanasto.lisaa_sana("python")
sanasto.lisaa_sana("olio")
sanasto.lisaa_sana("olio-ohjelmointi")
sanasto.lisaa_sana("olio")
sanasto.lisaa_sana("nörtti")

sanasto.tulosta()
Esimerkkitulostus

nörtti olio olio-ohjelmointi python

Luokka tallentaa sanalistan oliomuuttujaan self.sanat. Tässä tapauksessa self tarvitaan ehdottomasti sekä luokan konstruktorissa että luokan muissa metodeissa tähän muuttujaan viitatessa, koska muuten sama lista ei ole kaikkien olion metodien käytettävissä.

Lisätään luokalle metodi pisin_sana(self) joka selvittää nimensä mukaisesti sanaston pisimmän sanan (tai yhden niistä).

Tehtävän voisi toteuttaa vaikkapa seuraavasti, mutta näemme kohta miksei se ole kovin hyvä idea:

class Sanasto:
    def __init__(self):
        self.sanat = []

    # ...

    def pisin_sana(self):
        # määritellään kaksi apumuuttujaa
        self.pisin = ""
        self.pisimman_pituus = 0

        for sana in self.sanat:
            if len(sana) > self.pisimman_pituus:
                self.pisimman_pituus = len(sana)
                self.pisin = sana

        return self.pisin

Metodi siis käyttää kahta apumuuttujaa, jotka on määritelty käyttäen self-määrettä. Jos vielä halutaan hämmentää ohjelmakoodia lukevaa, apumuuttujat voisi lisäksi nimetä kryptisemmin, esim. apu ja apu2:

class Sanasto:
    def __init__(self):
        self.sanat = []

    # ...

    def pisin_sana(self):
        # määritellään kaksi apumuuttujaa
        self.apu = ""
        self.apu2 = 0

        for sana in self.sanat:
            if len(sana) > self.apu2:
                self.apu2 = len(sana)
                self.apu = sana

        return self.apu

Kun muuttujan määrittely tehdään self-määreen avulla, liitetään muuttuja olion attribuutiksi, eli muuttuja tulee olemaan edelleen olemassa myös metodin suorituksen päätyttyä. Tämä on aivan tarpeetonta, koska kyseisiä apumuuttujia on tarkoitus käyttää vain metodissa pisin_sana(self). Apumuuttujien määrittely self-määreen avulla on siis varsin huono idea.

Paitsi turhaa, apumuuttujien liittäminen self-määreella olion attribuuteiksi on myös riskialtista, varsinkin epämääräisesti nimettyjen apumuuttujien tapauksessa. Jos samaa apumuuttujaa self.apu käytetään monessa eri metodissa mutta täysin eri tarkoituksiin, voivat seuraukset olla arvaamattomat ja koodissa voi ilmetä hankalasti löydettäviä bugeja.

Ongelma voi tulla esiin erityisesti silloin jos apumuuttujan alkuarvo annetaan jossain muualla, esimerkiksi konstruktorissa:

class Sanasto:
    def __init__(self):
        self.sanat = []
        # määritellään apumuuttujia
        self.apu = ""
        self.apu2 = ""
        self.apu3 = ""
        self.apu4 = ""

    # ...

    def pisin_sana(self):
        for sana in self.sanat:
            # tämä ei toimi sillä apu2:n tyyppi on väärä
            if len(sana) > self.apu2:
                self.apu2 = len(sana)
                self.apu = sana

        return self.apu

Toisaalta uusien olion attribuuttien määrittely muualla kuin konstruktorissa on sikäli vaarallista, että tällöin olion attribuutit riippuvat siitä, mitä metodeja on suoritettu. Kaikilla saman luokan avulla luoduilla olioilla ei välttämättä ole samoja attribuutteja, mistä seuraa helposti bugeja.

Siispä oikea tapa määritellä yhdessä metodissa käytettävät apumuuttujat on tehdä se ilman self-määrettä:

class Sanasto:
    def __init__(self):
        self.sanat = []

    # ...

    def pisin_sana(self):
        # tämä on oikea tapa määritellä yhden metodin sisäiset apumuuttujat
        pisin = ""
        pisimman_pituus = 0

        for sana in self.sanat:
            if len(sana) > pisimman_pituus:
                pisimman_pituus = len(sana)
                pisin = sana

        return pisin

Tällaisessa toteutuksessa apumuuttujat ovat olemassa ainoastaan metodin suorituksen aikana, ja niissä olevat arvot eivät pääse aiheuttamaan komplikaatioita muussa koodissa.

Oliot funktioiden parametrina

Omista luokista luodut oliot ovat yleensä muuttuvia eli mutatoituvia, joten niiden toiminta parametrina välitettäessä muistuttaa esimerkiksi listoista tuttua tapaa: funktio, jolle olio välitetään parametrina, voi muuttaa kyseistä oliota.

Tarkastellaan yksinkertaista esimerkkiä, jossa funktiolle välitetään Opiskelija-luokasta luotu olio. Funktion sisällä muutetaan opiskelijan nimi, ja muutos näkyy myös pääohjelmassa, koska molemmissa tilanteissa viitataan samaan olioon.

class Opiskelija:
    def __init__(self, nimi: str, opiskelijanumero: str):
        self.nimi = nimi
        self.opiskelijanumero = opiskelijanumero

    def __str__(self):
        return f"{self.nimi} ({self.opiskelijanumero})"

# Huomaa, että tyyppivihjeenä käytetään nyt oman luokan nimeä
def muuta_nimi(opiskelija: Opiskelija):
    opiskelija.nimi = "Olli Opiskelija"

# Luodaan opiskelijaolio
olli = Opiskelija("Olli Oppilas", "12345")

print(olli)
muuta_nimi(olli)
print(olli)
Esimerkkitulostus

Olli Oppilas (12345) Olli Opiskelija (12345)

Olion voi myös luoda funktion sisällä. Mikäli funktio palauttaa viittauksen olioon, on muodostettu olio käytettävissä myös pääohjelmassa:

from random import randint, choice

class Opiskelija:
    def __init__(self, nimi: str, opiskelijanumero: str):
        self.nimi = nimi
        self.opiskelijanumero = opiskelijanumero

    def __str__(self):
        return f"{self.nimi} ({self.opiskelijanumero})"


# Funktio luo ja palauttaa Opiskelija-olion, jolla on satunnainen nimi ja opiskelijanumero
def uusi_opiskelija():
    etunimet = ["Arto","Pekka","Minna","Mari"]
    sukunimet = ["Virtanen", "Lahtinen", "Leinonen", "Pythonen"]

    # arvo nimi
    nimi = choice(etunimet) + " " + choice(sukunimet)

    # Arvo opiskelijanumero
    opiskelijanumero = str(randint(10000,99999))

    # Luo ja palauta opiskelijaolio
    return Opiskelija(nimi, opiskelijanumero)

if __name__ == "__main__":
    # Kutsutaan metodia viidesti, tallennetaan tulokset listaan
    opiskelijat = []
    for i in range(5):
        opiskelijat.append(uusi_opiskelija())

    # Tulostetaan
    for opiskelija in opiskelijat:
        print(opiskelija)
Esimerkkitulostus

Mari Lahtinen (36213) Arto Virtanen (11859) Mari Pythonen (77330) Arto Pythonen (86451) Minna Pythonen (86211)

Oliot metodien parametrina

Oliot toimivat normaaliin tapaan myös metodien parametrina. Tarkastellaan seuraavaa esimerkkiä:

class Henkilo:
    def __init__(self, nimi: str, pituus: int):
        self.nimi = nimi
        self.pituus = pituus

class Huvipuistolaite:
    def __init__(self, nimi: str, pituusraja: int):
        self.kavijoita = 0
        self.nimi = nimi
        self.pituusraja = pituusraja

    def ota_kyytiin(self, henkilo: Henkilo):
        if henkilo.pituus >= self.pituusraja:
            self.kavijoita += 1
            print(f"{henkilo.nimi} pääsi kyytiin")
        else:
            print(f"{henkilo.nimi} liian lyhyt :(")

    def __str__(self):
        return f"{self.nimi} ({self.kavijoita} kävijää)"

Huvipuistolaitteen metodi ota_kyytiin saa nyt parametrina luokan Henkilo olion. Jos kävijä on riittävän pitkä, metodi päästää hänet laitteeseen ja lisää kävijöiden määrää. Seuraavassa esimerkkisuoritus:

hurjakuru = Huvipuistolaite("Hurjakuru", 120)
jarkko = Henkilo("Jarkko", 172)
venla = Henkilo("Venla", 105)

hurjakuru.ota_kyytiin(jarkko)
hurjakuru.ota_kyytiin(venla)

print(hurjakuru)
Esimerkkitulostus

Jarkko pääsi kyytiin Venla liian lyhyt :( Hurjakuru (1 kävijää)

Loading
Loading

Saman luokan oliot metodien parametrina

Tarkastellaan jälleen kerran yhtä versiota luokasta Henkilo:

class Henkilo:
    def __init__(self, nimi: str, syntynyt: int):
        self.nimi = nimi
        self.syntynyt = syntynyt

Oletetaan että olemme tekemässä ohjelmaa, joka vertailee henkilöiden ikiä. Voisimme tehdä tarkoitusta varten erillisen funktion:

def vanhempi_kuin(henkilo1: Henkilo, henkilo2: Henkilo):
    if henkilo1.syntynyt < henkilo2.syntynyt:
        return True
    else:
        return False

muhammad = Henkilo("Muhammad ibn Musa al-Khwarizmi", 780)
pascal = Henkilo("Blaise Pascal", 1623)
grace = Henkilo("Grace Hopper", 1906)

if vanhempi_kuin(muhammad, pascal):
    print(f"{muhammad} on vanhempi kuin {pascal}")
else:
    print(f"{muhammad} ei ole vanhempi kuin {pascal}")

if vanhempi_kuin(grace, pascal):
    print(f"{grace} on vanhempi kuin {pascal}")
else:
    print(f"{grace} ei ole vanhempi kuin {pascal}")
Esimerkkitulostus

Muhammad ibn Musa al-Khwarizmi on vanhempi kuin Blaise Pascal Grace Hopper ei ole vanhempi kuin Blaise Pascal

Olio-ohjelmoinnin henkeen kuuluu kuitenkin sijoittaa oliota käsittelevät "funktiot" luokan metodeiksi. Voisimmekin tehdä henkilölle metodin, jonka avulla henkilön ikää voidaan verrata toiseen henkilöön:

class Henkilo:
    def __init__(self, nimi: str, syntynyt: int):
        self.nimi = nimi
        self.syntynyt = syntynyt

    # huomaa, että tyyppivihje pitää antaa hipsuissa jos parametri on saman luokan olio!
    def vanhempi_kuin(self, toinen: "Henkilo"):
        if self.syntynyt < toinen.syntynyt:
            return True
        else:
            return False

Nyt siis olio itse on self ja toinen on henkilöolio, joka toimii vertailukohtana.

Huomaa, miten metodin kutsuminen eroaa funktion kutsumisesta:

muhammad = Henkilo("Muhammad ibn Musa al-Khwarizmi", 780)
pascal = Henkilo("Blaise Pascal", 1623)
grace = Henkilo("Grace Hopper", 1906)

if muhammad.vanhempi_kuin(pascal):
    print(f"{muhammad.nimi} on vanhempi kuin {pascal.nimi}")
else:
    print(f"{muhammad.nimi} ei ole vanhempi kuin {pascal.nimi}")

if grace.vanhempi_kuin(pascal):
    print(f"{grace.nimi} on vanhempi kuin {pascal.nimi}")
else:
    print(f"{grace.nimi} ei ole vanhempi kuin {pascal.nimi}")

Pisteen vasemmalla puolella on siis verrattava henkilö, eli olio, johon metodin suorituksessa viittaa muuttuja self. Metodin parametrina taas on vertailukohta, eli metodin suorituksessa muuttujan toinen viittaama olio.

Ohjelman tulostus on sama kuin edellisessä funktiota käyttäneessä esimerkissä.

Huomaa, että if-else-rakenne metodissa vanhempi_kuin on oikeastaan turha, sillä vertailun arvona on suoraan haluamamme totuusarvo. Voimme siis yksinkertaistaa metodia seuraavasti:

class Henkilo:
    def __init__(self, nimi: str, syntynyt: int):
        self.nimi = nimi
        self.syntynyt = syntynyt

    # huomaa, että tyyppivihje pitää antaa hipsuissa jos parametri on saman luokan olio!
    def vanhempi_kuin(self, toinen: "Henkilo"):
        return self.syntynyt < toinen.syntynyt:

Edellisestä esimerkistä kannattaa huomata se, että kun metodi saa parametrikseen toisen saman luokan olion, tulee tyyppivihje antaa hipsuissa, eli seuraava koodi aiheuttaisi virheen:

class Henkilo:
    # ...

    # tämä ei toimi, Henkilo pitaa olla hipsuissa
    def vanhempi_kuin(self, toinen: Henkilo):
        return self.syntynyt < toinen.syntynyt:
Loading
Seuraava osa: