Ohjelmoinnin peruskurssi Y2, kurssimateriaali

Luku 3.3: Python olio-ohjelmointi

Etusivulle

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.

../_images/oliokasitteet1.png

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, poikkeus Exception.
  • Modulin määrittämät globaalit nimet. Esim. moduli random määrittää joukon nimiä (kuten seed, choice ja Random) 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:

  1. 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.
  2. Etsitään tästä listasta ensimmäinen luokka, joka määrittää haetun metodin.

Palaute

Vastaa palautekyselyyn A+:ssa.

Etusivulle