Luokkahierarkiat
Luokkien erikoistaminen
Joskus tulee vastaan tilanne, jossa luokan toimintaa olisi hyvä pyrkiä erikoistamaan, mutta vain osalle olioista. Tarkastellaan esimerkkinä tilannetta, jossa meillä on kaksi luokkaa - Opiskelija ja Opettaja. Yksinkertaistuksen vuoksi luokista on jätetty pois kaikki asetus- ja havainnointimetodit.
class Opiskelija:
def __init__(self, nimi: str, opnro: str, sposti: str, opintopisteet: str):
self.nimi = nimi
self.opnro = opnro
self.sposti = sposti
self.opintopisteet = opintopisteet
class Opettaja:
def __init__(self, nimi: str, sposti: str, huone: str, opetusvuosia: int):
self.nimi = nimi
self.sposti = sposti
self.huone = huone
self.opetusvuosia = opetusvuosia
Yksinkertaistetustakin esimerkistä huomataan, että luokilla on yhteisiä piirteitä - tässä tapauksessa nimi ja sähköpostiosoite. Monessa tilanteessa olisi hyvä, jos yhteisiä piirteitä voitaisin käsitellä yhdellä operaatiolla: oletetaan tilanne, jossa koulun sähköpostitunnus muuttuu. Toki voitaisiin kirjoittaa kaksi käsittelyfunktiota...
def korjaa_email(o: Opiskelija):
o.sposti = o.sposti.replace(".com", ".edu")
def korjaa_email2(o: Opettaja):
o.sposti = o.sposti.replace(".com", ".edu")
...mutta saman koodin toistaminen kahteen kertaan tuntuu turhalta työltä, ja lisää virheiden mahdollisuutta. Olisi siis hyvä, jos molempien luokkien mukaisia olioita voitaisiin käsitellä samalla metodilla.
Luokat kuitenkin sisältävät myös piirteitä, joita toisella luokalla ei ole. Sen takia luokkien yhdistäminen ei tunnu järkevältä.
Perintä
Ratkaisu löytyy olio-ohjelmoinnin tekniikasta nimeltä perintä. Perinnällä tarkoitetaan sitä, että luokka perii piirteet joltain toiselta luokalta. Näiden perittyjen piirteiden rinnalle luokka voi sitten toteuttaa uusia piirteitä.
Opettaja- ja Opiskelija-luokilla voisi olla yhteinen yliluokka Henkilo
:
class Henkilo:
def __init__(self, nimi: str, sposti: str):
self.nimi = nimi
self.sposti = sposti
Luokassa on toteutettu siis henkilöön liittyvät piirteet. Nyt luokat Opiskelija ja Opettaja voivat periä luokan ja lisätä perittyjen ominaisuuksien rinnalle uusia piirteitä:
Perintä tapahtuu kirjoittamalla luokan nimen perään perittävän luokan nimi sulkuihin:
class Henkilo:
def __init__(self, nimi: str, sposti: str):
self.nimi = nimi
self.sposti = sposti
def vaihda_spostitunniste(self, uusi_tunniste: str):
vanha = self.sposti.split("@")[1]
self.sposti = self.sposti.replace(vanha, uusi_tunniste)
class Opiskelija(Henkilo):
def __init__(self, nimi: str, opnro: str, sposti: str, opintopisteet: str):
self.nimi = nimi
self.opnro = opnro
self.sposti = sposti
self.opintopisteet = opintopisteet
class Opettaja(Henkilo):
def __init__(self, nimi: str, sposti: str, huone: str, opetusvuosia: int):
self.nimi = nimi
self.sposti = sposti
self.huone = huone
self.opetusvuosia = opetusvuosia
# Testi
if __name__ == "__main__":
olli = Opiskelija("Olli Opiskelija", "1234", "olli@example.com", 0)
olli.vaihda_spostitunniste("example.edu")
print(olli.sposti)
outi = Opettaja("Outi Ope", "outi@example.fi", "A123", 2)
outi.vaihda_spostitunniste("example.ex")
print(outi.sposti)
Koska sekä Opiskelija että Opettaja perivät luokan Henkilo, molemmilla on käytössään Henkilo-luokassa määritellyt piirteet, mukaanlukien metodi vaihda_spostitunniste
.
Tarkastellaan vielä toista esimerkkiä, jossa luokka Kirjahylly perii luokan Laatikko:
class Kirja:
""" Luokka mallintaa yksinkertaista kirjaa """
def __init__(self, nimi: str, kirjailija: str):
self.nimi = nimi
self.kirjailija = kirjailija
class Kirjalaatikko:
""" Luokka mallintaa laatikkoa, johon voidaan tallentaa kirjoja """
def __init__(self):
self.kirjat = []
def lisaa_kirja(self, kirja: Kirja):
self.kirjat.append(kirja)
def listaa_kirjat(self):
for kirja in self.kirjat:
print(f"{kirja.nimi} ({kirja.kirjailija})")
class Kirjahylly(Kirjalaatikko):
""" Luokka mallintaa yksinkertaista kirjahyllyä """
def __init__(self):
super().__init__()
def lisaa_kirja(self, kirja: Kirja, paikka: int):
self.kirjat.insert(paikka, kirja)
Luokassa Kirjahylly on määritelty metodi lisaa_kirja
. Samanniminen metodi on määritelty myös yliluokassa Kirjalaatikko
. Tällaisessa tapauksessa puhutaan metodin uudelleenmäärittelystä tai ylikirjoituksesta (overwriting): aliluokan samanniminen metodi korvaa yliluokan vastaavan metodin.
Esimerkissämme idea on, että kirjalaatikossa kirja asetetaan aina laatikossa päällimmäiseksi, mutta kirjahyllyssä voidaan määritellä asetuspaikka. Sen sijaan metodin listaa_kirjat
uudelleenmäärittelyä ei ole nähty tarpeelliseksi - sama kirjojen listaus toimii niin laatikossa kuin hyllyssäkin (ainakin esimerkissämme).
Tarkastellaan esimerkkiä luokkien käyttämisestä:
if __name__ == "__main__":
# Luodaan pari kirjaa testiksi
k1 = Kirja("7 veljestä", "Aleksis Kivi")
k2 = Kirja("Sinuhe", "Mika Waltari")
k3 = Kirja("Tuntematon sotilas", "Väinö Linna")
# Luodaan kirjalaatikko ja lisätään kirjat sinne
laatikko = Kirjalaatikko()
laatikko.lisaa_kirja(k1)
laatikko.lisaa_kirja(k2)
laatikko.lisaa_kirja(k3)
# Luodaan kirjahylly ja lisätään kirjat sinne (aina hyllyn alkupäähän)
hylly = Kirjahylly()
hylly.lisaa_kirja(k1, 0)
hylly.lisaa_kirja(k2, 0)
hylly.lisaa_kirja(k3, 0)
# Tulostetaan
print("Laatikossa:")
laatikko.listaa_kirjat()
print()
print("Hyllyssä:")
hylly.listaa_kirjat()
Laatikossa: 7 veljestä (Aleksis Kivi) Sinuhe (Mika Waltari) Tuntematon sotilas (Väinö Linna)
Hyllyssä: Tuntematon sotilas (Väinö Linna) Sinuhe (Mika Waltari) 7 veljestä (Aleksis Kivi)
Myös Kirjahylly-luokasta muodostettujen olioiden kautta voidaan käyttää metodia listaa_kirjat
, koska perinnän ansiosta se on olemassa myös luokan Kirjahylly
aliluokissa.
Piirteiden periytyminen
Aliluokka perii yliluokalta kaikki piirteet. Aliluokasta voidaan viitata suoraan yliluokan piirteisiin, paitsi jos yliluokassa on määritelty piirteet yksityisiksi (käyttämällä kahta alaviivaa muuttujan nimen edessä).
Niinpä esimerkiksi Kirjahylly-luokasta voitaisiin viitata yliluokan konstruktoriin sen sijaan että kirjoitettaisiin toiminnallisuus uudestaan:
class Kirjahylly(Kirjalaatikko):
def __init__(self):
super().__init__()
Yliluokan konstuktoriin (tai yliluokkaan muutenkin) viitataan funktion super()
avulla. Huomaa, että tässäkin tapauksessa parametri self
lisätään automaattisesti.
Tarkastellaan toisena esimerkkinä luokkaa Gradu, joka perii luokan Kirja. Aliluokasta kutsutaan yliluokan konstruktoria:
class Kirja:
""" Luokka mallintaa yksinkertaista kirjaa """
def __init__(self, nimi: str, kirjailija: str):
self.nimi = nimi
self.kirjailija = kirjailija
class Gradu(Kirja):
""" Luokka mallintaa gradua eli ylemmän korkeakoulututkinnon lopputyötä """
def __init__(self, nimi: str, kirjailija: str, arvosana: int):
super().__init__(nimi, kirjailija)
self.arvosana = arvosana
Nyt Gradu-luokan konstruktorista kutsutaan yliluokan (eli luokan Kirja) konstruktoria, jossa asetetaan attribuuttien nimi
ja kirjailija
arvot. Sen jälkeen aliluokan konstruktorissa asetetaan attribuutin arvosana
arvo - tätä luonnollisesti ei voida tehdä yliluokan konstruktorissa, koska yliluokalla ei tällaista attribuuttia ole.
Luokkaa voidaan käyttää esimerkiksi näin:
# Testataan
if __name__ == "__main__":
gradu = Gradu("Python ja maailmankaikkeus", "Pekka Python", 3)
# Tulostetaan kenttien arvot
print(gradu.nimi)
print(gradu.kirjailija)
print(gradu.arvosana)
Python ja maailmankaikkeus Pekka Python 3
Koska aliluokka Gradu
perii kaikki yliluokan piirteet, se perii myös attribuutit nimi
ja kirjailija
. Arvot osalle attribuuteista annetaan yliluokan sisältä löytyvässä konstruktorissa.
Aliluokka voi myös viitata yliluokan metodiin, vaikka metodi olisikin määritelty uudestaan aliluokassa. Seuraavassa esimerkissä luokasta Platinakortti
kutsutaan uudelleenmääritellyssä metodissa bonuspisteet
yliluokan vastaavaa metodia.
class Tuote:
def __init__(self, nimi: str, hinta: float):
self.nimi = nimi
self.hinta = hinta
class Bonuskortti:
def __init__(self):
self.ostetut_tuotteet = []
def lisaa_tuote(self, tuote: Tuote):
self.ostetut_tuotteet.append(tuote)
def laske_bonus(self):
bonus = 0
for tuote in self.ostetut_tuotteet:
bonus += tuote.hinta * 0.05
return bonus
class Platinakortti(Bonuskortti):
def __init__(self):
super().__init__()
def laske_bonus(self):
# Kutsutaan yliluokan metodia...
bonus = super().laske_bonus()
# ...ja lisätään vielä viisi prosenttia päälle
bonus = bonus * 1.05
return bonus
Nyt platinakortin bonus lasketaan hyödyntämällä aluksi yliluokan vastaavaa metodia ja lisäämällä sitten ylimääräiset 5 prosenttia tähän bonukseen. Esimerkki luokkien käytöstä:
if __name__ == "__main__":
kortti = Bonuskortti()
kortti.lisaa_tuote(Tuote("Banaanit", 6.50))
kortti.lisaa_tuote(Tuote("Mandariinit", 7.95))
bonus = kortti.laske_bonus()
kortti2 = Platinakortti()
kortti2.lisaa_tuote(Tuote("Banaanit", 6.50))
kortti2.lisaa_tuote(Tuote("Mandariinit", 7.95))
bonus2 = kortti2.laske_bonus()
print(bonus)
print(bonus2)
0.7225 0.7586250000000001