Luku 3.3: Python olio-ohjelmointi
Esitietokurssilla CSE-A1111 Ohjelmoinnin peruskurssi Y1 käsiteltiin jonkin verran luokkia ja olioita. Jos perusteet ovat päässeet unohtumaan, voi käydä läpi kurssimonisteesta luvun 7: Luokat ja oliot.
Tässä luvussa perehdymme hieman tarkemmin olio-ohjelmointiin. Ulkoisena lähdemateriaalina on
Tämä kannattaa pitää mielessä ohjelmointitehtäviä ja omaa projektia tehtäessä.
Oliokäsitteet
Oheisessa kuvassa on hahmotettu olio-ohjelmointiin liittyviä käsitteitä ja niiden välisiä suhteita.
Termit ovat kuvassa englanniksi. Myös monia vaihtoehtoisia termejä esiintyy ja mekin käytämmä toisinaan muita termejä kuin kuvassa. Esimerkiksi termi Property (suom. Ominaisuus) on alla olevassa tekstissä (ainakin pääosin) Attribuutti.
Olioiden nimistä ja viittauksista
Muistutuksena aluksi miten sijoitus Pythonissa toimii, kun arvona on olio. Luodaan pari oliota.
>>> a = object()
>>> b = object()
>>> a == b
False
>>> c = a
>>> c == a
True
>>> a
<object object at 0x10a961090>
>>> b
<object object at 0x10a9610a0>
>>> c
<object object at 0x10a961090>
Kuten huomataan a = object()
ja b = object()
luovat kaksi eri oliota sekä kaksi nimeä a
ja
b
, jotka viittaavat luotuihin olioihin. Sen sijaan sijoitus c = a
ei luo yhtään uutta oliota vaan
tuottaa uuden nimen oliolle, johon a
jo viittasi.
Nimiavaruudet ja näkyvyysalueet
Luokkamäärittelyn selittämistä varten on hyvä ensin puhua nimiavaruuksista ja näkyvyysalueista. Jos tämä tuntuu ensiksi hämärältä, älä huolestu. Tähän voi palata myöhemmin, kun on saanut hieman kokemusta olioiden ja luokkien käsittelystä. Pythonin nimiavaruudet ja näkyvyysalueet on määritetty hieman sekavasti; jos haluaa perehtyä asiaan tarkemmin, kannattaa lukea niistä lisää Pythonin dokumentaatiosta.
Nimiavaruus
Nimiavaruus on joukko sidontoja, eli nimi - arvo -pareja. Sijoituslauseet sekä funktion ja luokkien määritykset tuottavat sidontoja ja lisäävät ne johonkin nimiavaruuteen. Nimiavaruutta voi ajatella Pythonin dict-oliona, jossa nimet ovat avaimia.
Pythonissa on käytössä useita nimiavaruuksia:
- Pythonin sisäänrakennetut nimet, esim. funktio
abs
, poikkeusException
. - Modulin määrittämät globaalit nimet. Esim. moduli
random
määrittää joukon nimiä (kutenseed
,choice
jaRandom
) ja nämä muodostavat nimiavaruuden. - Funktion (tai oikeammin funktion kutsun) määrittämät paikalliset nimet. Tämä nimiavaruus sisältää funktion parametrien ja paikallisten muuttujien nimet.
- Luokan (luokkaolion) määrittämät paikalliset nimet.
On tärkeää havaita, että eri nimiavaruuksissa esiintyvät saman näköiset nimet eivät oikeasti tarkoita samaa nimeä, eli ne eivät (yleensä) viittaa samaan olioon.
Nimiavaruuksia luodaan eri aikoina ohjelman suoritusta ja ne myös ovat olemassa vaihtelevan ajan. Sisäänrakennetut nimet sisältävä nimiavaruus luodaan Pythonin käynnistyessä eikä se katoa ennen kuin ohjelman suoritus päättyy. Modulin globaali nimiavaruus luodaan, kun Python lukee sisään modulin ja yleensä säilyy suorituksen loppuun asti. Funktiokutsuun liittyvä nimiavaruus luodaan funktiokutsun yhteydessä ja se lakkaa olemasta, kun funktionkutsusta palataan. Jos on sisäkkäisiä kutsuja samaan funktioon, kullekin luodaan oma nimiavaruus.
Näkyvyysalue
Näkyvyysalue puolestaan on ohjelmakoodissa näkyvä yhtenäinen alue, jossa tietty nimiavaruus on suoraan käytettävissä (ei siis tarvitse käyttää
pistenotaatiota x.y
jos halutaan viitata nimiavaruuden x nimeen y). Tällaisia näkyvyysalueita on monia:
- Funktiomäärittely
- Luokkamäärittely
- Modulimääritys
Näkyvyysalueet voivat esiintyä sisäkkäin. Esimerkiksi luokkamäärittelyn sisältämä funktion määrittely muodostaa oman näkyvyysalueen ja tekee ikäänkuin reiän ympäröivän luokan määrittelyyn. Jos sama nimi on määritetty sisäkkäisissä näkyvyysalueissa, viittaa nimi aina lähimmän ympäröivän näkyvyysalueen määrittämää nimeä.
Luokkamäärittely
Tarkastellaan nyt yksinkertaista Pythonin luokkamäärittelyä, joka on muotoa:
class Luokka:
<lause-1>
.
.
.
<lause-N>
Kun Python näkee tällaisen luokkamäärittelyn, alkaa se suorittaa sitä. Suorituksen tuloksena syntyy luokkaolio ja sidonta luokan nimen ja tämän luokkaolion välille. Tämä sidonta lisätään käsillä olevaan lähimpään paikalliseen nimiavaruuteen. Esimerkiksi jos luokkamäärittely on modulin päätasolla, lisätään modulin nimiavaruuteen sidonta luokan nimen ja luokkaolion välille. Jos taasen luokkamäärittely on toisen luokkamäärittelyn sisällä, lisätään sidonta tämän ulomman luokan nimiavaruuteen.
Miten luokkamäärittelyn suoritus tapahtuu? Ensiksi Python luo uuden nimiavaruuden uutta luokkaa varten. Luokamäärityksen sisältämät lauseet voivat olla mitä tahansa Python-lauseita ja Python suorittaa ne tässä nimiavaruudessa. Kaikki sijoitukset paikallisiin muuttujiin luovat uusia sidontoja tässä nimiavaruudessa. Tyypillisimmin luokan määrittely sisältää funktion määrittelyjä ja niiden suorittaminen saa aikaan sen, että määritellyt funktioiden nimet sidotaan funktioiden määrittelyihin (funktio-olioihin).
Luokkaolio ja instanssioliot
Luokkamäärittelyn suoritus tuotti siis uuden luokkaolion, jolla on joukko attribuutteja.
class Ihminen:
def __init__(self, nimi, ika, paino, pituus):
self.nimi = nimi
self.ika = ika
self.paino = paino
self.pituus = pituus
self.bmi = self.paino/(self.pituus/100)**2.
def vanhene(self):
self.ika += 1
Tutkaillaan hieman luokkaoliota:
>>> attrs=set(dir(Ihminen))
>>> dictKeys=set(Ihminen.__dict__.keys())
>>>
>>> print("""
... Luokan Ihminen kaikki attribuutit == {attrs}
...
... Luokan Ihminen nimiavaruuden kaikki nimet == {dictKeys}
...
... Joukkoerotus attribuutit-nimet == {diff1}
...
... Joukkoerotus attribuutit-nimet == {diff2}
... """.format(attrs=attrs, dictKeys=dictKeys, diff1=attrs-dictKeys, diff2=dictKeys-attrs))
Luokan Ihminen kaikki attribuutit == {'__gt__', '__new__', '__delattr__', 'vanhene', '__init__', '__reduce_ex__',
'__getattribute__', '__format__', '__eq__', '__ne__', '__weakref__', '__le__', '__reduce__', '__lt__', '__repr__',
'__module__', '__dict__', '__doc__', '__sizeof__', '__setattr__', '__subclasshook__', '__class__', '__ge__',
'__dir__', '__hash__', '__str__'}
Luokan Ihminen nimiavaruuden kaikki nimet == {'__module__', '__dict__', 'vanhene', '__init__', '__weakref__',
'__doc__'}
Joukkoerotus attribuutit-nimet == {'__reduce__', '__setattr__', '__subclasshook__', '__class__', '__gt__', '__lt__',
'__repr__', '__getattribute__', '__new__', '__format__', '__delattr__', '__ge__', '__eq__', '__dir__', '__ne__',
'__hash__', '__le__', '__reduce_ex__', '__str__', '__sizeof__'}
Joukkoerotus attribuutit-nimet == set()
Kuten nähdään, attribuutteja on enemmän kuin nimiavaruuden nimiä. Nimiavaruuden nimet ovat itse asiassa osajoukko kaikista attribuuteista.
Mitä nämä nimet tarkoittavat?
Nimiavaruudessa on määrittämämme metodit __init__
ja vanhene
. Lisäksi siellä on
nimiavaruuden sisältävä __dict__
, ympäröivään moduliin viittaava __module__
sekä luokan
dokumentaatiomerkkijono __doc__
sekä Pythonin toteutuksessa käytettävä __weakref__
(jos kiinnostaa,
katso 8.8. weakref — Weak references).
Attribuuteissa on näiden lisäksi kaikenlaista himmeältä vaikuttavaa kamaa, kuten __subclasshook__
, johon emme
puutu tässä. Lisäksi siellä on Pythonin ymmärtämiä erikoisnimiä, kuten __str__
, joka viittaa metodiin jolla
tuotetaan oliota kuvaava merkkijono. Tämän voi määrittää uudestaan. Nimet __ge__
, __eq__
jne. ovat
vertailuoperaattoreiden kuten '<', '==', jne. toteuttavia metodeja. Näitä uudelleenmäärittämällä voidaan muuttaa
olioiden vertailua ja luoda olioiden välinen suuruusjärjestys.
Luodaan nyt ilmentymä luokasta Ihminen. Kuten muistetaan, se tapahtuu ikäänkuin kutsuttaisiin funktiota nimeltä Ihminen()
. Katsotaan sitten,
mitä saatiin aikaan:
>>> maija=Ihminen(nimi="Maija Metso", ika=35, paino=50, pituus=165)
>>>
>>> attrs=set(dir(maija))
>>> dictKeys=set(maija.__dict__.keys())
>>>
>>> print("""
... Olion maija kaikki attribuutit == {attrs}
...
... Olion maija nimiavaruuden kaikki nimet == {dictKeys}
...
... Joukkoerotus attribuutit-nimet == {diff1}
...
... Joukkoerotus attribuutit-nimet == {diff2}
... """.format(attrs=attrs, dictKeys=dictKeys, diff1=attrs-dictKeys, diff2=dictKeys-attrs))
Olion maija kaikki attribuutit == {'__gt__', '__new__', '__delattr__', 'vanhene', '__init__', 'ika', 'bmi',
'__reduce_ex__', 'nimi', '__getattribute__', '__format__', '__eq__', '__ne__', '__weakref__', '__le__', '__reduce__',
'__lt__', '__repr__', '__module__', 'paino', '__dict__', '__doc__', '__sizeof__', '__setattr__', '__subclasshook__',
'pituus', '__class__', '__ge__', '__dir__', '__hash__', '__str__'}
Olion maija nimiavaruuden kaikki nimet == {'paino', 'pituus', 'bmi', 'nimi', 'ika'}
Joukkoerotus attribuutit-nimet == {'__reduce__', '__gt__', '__lt__', '__repr__', '__new__', '__module__',
'__delattr__', '__dict__', 'vanhene', '__init__', '__reduce_ex__', '__doc__', '__sizeof__', '__setattr__',
'__subclasshook__', '__class__', '__ge__', '__getattribute__', '__format__', '__dir__', '__eq__', '__ne__',
'__hash__', '__weakref__', '__le__', '__str__'}
Joukkoerotus attribuutit-nimet == set()
Taas näemme, että nimiavaruuden nimet on attribuuttien nimien osajoukko.
Miksi self?
Luokan metodien määrityksissä on aina ensimmäisenä parametri, joka viittaa käsiteltävään olioon. Tyypillisesti siitä käytetään nimeä self
,
mutta periaatteessa mikä tahansa nimi kelpaisi: this
, me
, einari
.
>>> class Eikka:
... def __init__(einari):
... einari.ika = 100
...
>>> e=Eikka()
>>> e.ika
100
Lienee kuitenkin suotavaa selkeyden vuoksi käyttää nimeä
self
tuolle parametrille.
Funktio-oliot ja metodioliot
Luokassa Ihminen määritetyn metodin nimi vanhene
esiintyy sekä luokkaolion Ihminen että instassiolion maija
attribuuteissa (mutta ainostaan Ihmisen nimiavaruudessa, miksi?). Onko kyseessä sama asia?
>>> Ihminen.vanhene
<function Ihminen.vanhene at 0x10ab04ae8>
>>> maija.vanhene
<bound method Ihminen.vanhene of <__main__.Ihminen object at 0x10ab48048>>
>>>
>>> type(maija.vanhene)
<class 'method'>
>>> type(Ihminen.vanhene)
<class 'function'>
Eipä ollutkaan! Pythonissa sekä funktiot että metodit ovat
olioita. (ks. The Python Standard Library: 4.12.3. Functions ja 4.12.4. Methods).
Instanssioliolle maija on luotu metodi, joka ei ota yhtään parametria vaan joka osaa itse hakea metodissa käytettävän
parametrin self
arvoksi olion maija. Seuraavassa vanhennamme maijaa kahdella eri tavalla:
>>> maija.ika
35
>>> maija.vanhene()
>>> maija.ika
36
>>> Ihminen.vanhene(maija)
>>> maija.ika
37
Tässä maijan metodi ja Ihmisen funktio tuottavat saman tuloksen: ikä lisääntyy yhdellä. Perinnän yhteydessä
Luokka.funktio(instanssi)
ei välttämättä tuota samaa tulosta kuin instanssi.funktio()
(miksi?).
Voimme kutsua olion metodeja myös ilman pistenotaatiota.
>>> maija.ika
37
>>>
>>> v=maija.vanhene
>>>
>>> v() # Maija vanhenee
>>> maija.ika
38
Yllä oleva esimerkki voi vaikuttaa mielettömältä — miksi emme suoraan kutsu metodia? Toisinaan on kuitenkin näppärää välittää operaatio kutsuttavaksi muualla koodissa ilman että samalla välitämme oliota, johon operaatio kohdistuu.
Luokkamuuttujat ja instanssimuuttujat
Oliot voivat käsitellä kahdenlaisia muuttujia: luokkamuuttujia ja instanssimuuttujia. Instanssimuuttujia olemme nähneet
jo runsaasti esimerkiksi yllä self.pituus
ja self.ika
. Jokaisella oliolla on omat sidontansa kaikille
instanssimuuttujille eikä yhden olion instanssimuuttujan arvon tai sidonnan muutos muuta toisten saman luokan olioiden
instanssimuuttujien arvoja.
Luokkamuuttujat sen sijaan ovat yhteisiä kaikille luokan instansseille. Jos joku muuttaa luokkamuuttujan arvoa, näkevät
kaikki muutoksen seuraukset. Seuraavassa esimerkissä määritetään pari luokkamuuttujaa laji
ja taidot
.
Esimerkki havainnollistaa kahta luokkamuuttujiin liittyvää väärinkäsitystä.
Esimerkki: sekoilua luokkamuuttujien kanssa
class Ihminen:
laji = "Homo sapiens"
taidot = []
def __init__(self, nimi, ika, paino, pituus):
self.nimi = nimi
self.ika = ika
self.paino = paino
self.pituus = pituus
self.bmi = self.paino/(self.pituus/100)**2.
def vanhene(self):
self.ika += 1
Käytetään sitten tätä luokkaa:
>>> maija=Ihminen(nimi="Maija Metso", ika=35, paino=50, pituus=165)
>>> kaarlo=Ihminen(nimi="Kaarlo Käki", ika=30, paino=75, pituus=180)
>>> maija.taidot.append('Lukee')
>>> kaarlo.taidot.append('Kirjoittaa')
>>> maija.taidot
['Lukee', 'Kirjoittaa', 'Lukee', 'Kirjoittaa']
>>> kaarlo.taidot
['Lukee', 'Kirjoittaa', 'Lukee', 'Kirjoittaa']
Muita oliokieliä, kuten Javaa tai C++:aa käyttänyt voisi kuvitella, että lause taidot = []
tuottaisi jokaiselle
instanssille oman muuttujan taidot
, mutta Pythonissa näin ei siis ole. Meidän pitäisi siis siirtää muuttujan
taidot
määritys luontimetodin sisälle muodossa: self.taidot = []
.
Tämän selvittyä seuraava yllätys muita oliokieliä käyttäneille voi olla, että luokkamuuttujan sidonnan muuttaminen ei muuta sidontaa kaikissa luokan instansseissa.
>>> maija.laji
'Homo sapiens'
>>> kaarlo.laji
'Homo sapiens'
>>> maija.laji = "Homo sapiens sapiens"
>>> maija.laji
'Homo sapiens sapiens'
>>> kaarlo.laji
'Homo sapiens'
Kaarlolla siis säilyi vanha sidonta muuttujalle laji
!
Tutkitaan olioiden nimiavaruuksia:
>>> maija.__dict__
{'laji': 'Homo sapiens sapiens', 'pituus': 165, 'nimi': 'Maija Metso', 'ika': 35,
'bmi': 18.36547291092746, 'paino': 50}
>>> kaarlo.__dict__
{'pituus': 180, 'nimi': 'Kaarlo Käki', 'ika': 30, 'bmi': 23.148148148148145,
'paino': 75}
>>>
Sijoitus maija.laji
on saanut aikaan sen, että maijan nimiavaruuteen on lisätty uusi sidonta laji='Homo
sapiens sapiens'
. Tätä tekniikkaa kutsutaan nimelle copy-on-write.
Perintä
Ohjelman suunnittelua käsiteltäessä luvussa 1.2 käytimme jo perintää. Havaitsimme esimerkiksi, että luokat Luento, Tentti, Harjoitus, Tapaaminen ja Projekti voisivat olla luokan KurssinOsa alaluokkia. Pythonissa perintä on suoraviivaista:
class Luokka(Yläluokka):
<lause-1>
.
.
.
<lause-N>
Aiemmin käyttämäämme muotoon on ainoastaan lisätty määritettävän luokan nimen jälkeen suluissa yläluokan nimi.
Python suorittaa tällaisen määritelmän muuten aivan samoin kuin ilman yläluokkaa. Ainoastaan viittaukset metodeihin käsitellään toisin. Jos metodia ei ole määritetty käsillä olevassa luokassa, etsitään sen määritystä yläluokasta ja jos se ei löydy sieltä, niin yläluokan yläluokasta jne.
Yläluokassa määritettyjä metodeja voi (ja usein kannattaa) määritellä uudestaan alaluokassa. Suurimpia hyötyjä perinnästä syntyy juuri siitä, että yläluokka määrittää jonkun toiminnallisuuden jota sitten alaluokissa muutetaan.
Määritettäessä alaluokassa uudelleen yläluokan metodia, voidaan myös yläluokan metodia kutsua. Tästä oli vihje yllä, kun kutsuimme luokan Ihminen metodia vanhene funktiona. Voimme toimia samoin mille tahansa metodille:
>>> class Base:
... def __init__(self, x):
... self.x = x
... def f(self):
... print("Hello {}".format(self.x))
...
>>> class Derived(Base):
... def __init__(self, x, y):
... Base.__init__(self, x)
... self.y = y
... def f(self):
... Base.f(self)
... print("Hello again {}".format(self.y))
...
>>> b=Base(10)
>>> d=Derived(20, 'foo')
>>> b.f()
Hello 10
>>> d.f()
Hello 20
Hello again foo
Moniperintä
Toisinaan haluaisimme määrittää luokan, joka yhdistää toiminnallisuutta useasta toisesta luokasta. Yksi tapa tähän on moniperintä. Pythonissa se tapahtuu seuraavanlaisella määrittelyllä:
class Luokka(Yläluokka1, Yläluokka2, ...):
<lause-1>
.
.
.
<lause-N>
Tässä JohdettuLuokka
käyttää yläluokkinaan luokkia PerusLuokka1
, PerusLuokka2
jne.
Jottei elämä olisi liian helppoa, on Pythonissa ollut kaksi tapaa käsitellä moniperintää. Näistä käytetään termejä New Class ja Classic Class. Python 3:ssa käytössä on onneksi enää uusi tapa.
Metodinhaku toimii seuraavasti:
- Muodostetaan lista kaikista yläluokista syvyyshaun avulla. Listassa on ensin yläluokka1, sitten yläluokan1 ensimmäinen yläluokka jos sellainen on jne. Vasta kun kaikki yläluokan1 esivanhempiluokat on listattu, aletaan samalla tavalla listata yläluokkaa2 ja sen esivanhempiluokkia. Näin syntyneestä listasta poistetaan kaikki duplikaatit. Moniperintä voi aikaansaada niin sanottuja timanttirakenteita, joissa tietyn luokan yläluokilla on yhteinen esivanhempiluokka. Tällaisen rakenteen kohdalla kyseinen esivanhempi otetaan ainoastaan kerran.
- Etsitään tästä listasta ensimmäinen luokka, joka määrittää haetun metodin.
Palaute
Vastaa palautekyselyyn A+:ssa.