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)
Matematiikka 1 Ohjelmointi 1 Fysiikka 2 Ohjelmointi 2 Opintopisteitä yhteensä: 20
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"))
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)
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)
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:
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()
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)
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)
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)
Jarkko pääsi kyytiin Venla liian lyhyt :( Hurjakuru (1 kävijää)
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}")
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: