1.2. 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 Y1-kurssin kurssimonisteen 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ä.
Ensiksi pyrimme löytämään motivaation olioiden ja luokkien käyttöön. Samalla perehdymme muutamiin keskeisiin olio-ohjelmoinnin mekanismeihin Pythonissa.
Mihin olioita ja luokkia tarvitaan
Yksinkertaisessa Python-ohjelmoinnissa käytetään yleensä Pythonin valmiita tyyppejä ja funktioita sekä muuttujia, lausekkeita ja lauseita. Usein määritetään funktioita toistuvien operaatioiden suorittamiseksi. Tietokokoelmia voidaan käsitellä näppärästi Pythonin valmiilla kokoelmatyypeillä kuten listoilla (List), monikoilla (Tuple), joukoilla (set, frozenset) sekä sanakirjoilla (dict). Näillä pärjätään hyvin, kun tehdään pieniä ohjelmia tai skriptejä jonkin eteen tulevan pikku tehtävän ratkaisemiseen.
Kun ratkottavat ongelmat kasvavat, niiden ratkaisu yllä mainituilla välineillä käy työlääksi. Tarkastellaan esimerkkiä.
Esimerkki: Geometriset kuviot
Tarvitaan ohjelma, jolla voi käsitellä geometrisia kuvioita. Aloitetaan vaikka ympyröistä. Ympyrän määrittää sen keskipiste ja säde. Kenties haluamme piirtää ympyrän, jolloin meitä kiinnostaa myös sen väri.
Seuraavassa teemme erilaisia ohjelmaversioita tämän ongelman ratkaisuun ilman olioita ja luokkia ja lopulta niiden avulla. Tässä tekstissä selkeyden vuoksi emme näytä jokaisen version kaikkea koodia vaan ainoastaan oleelliset kohdat. Jos haluat kokeilla ohjelmia, voit tallentaa ne tekstissä annetuista linkeistä. Jos haluat heti nähdä, miltä lopullinen oliopohjainen ratkaisu näyttää, hyppää kohtaan Versio 7: Ympyrät olioina.
Versio 1: Ympyrän tiedot useassa muuttujassa
Kokeillaan ensin ratkaista ongelma käyttäen Pythonin muuttujia.
from math import pi
center = (10.0, 20.0)
radius = 2.5
color = (168, 201, 255)
area = pi * radius ** 2
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(center, radius, area, color))
Tämän version löydät tiedostosta circle_no_oo1.py.
Ratkaisun puutteet
Tällä tavoin voimme esittää vain yhden ympyrän, mutta arvatenkin haluamme haluamme käsitellä useampia ympyröitä.
Versio 2: Monen ympyrän tiedot numeroiduissa muuttujissa
Mitä, jos määrittäisimme muuttujat center1
, center2
, center3
, ... ja vastaavasti radius1
, radius2
,
radius3
, ... sekä color1
, color2
, color3
... seuraavasti:
...
center1 = (10.0, 20.0)
center2 = (0.0, -10.0)
center3 = (10.5, 19.5)
radius1 = 2.5
radius2 = 7.5
radius3 = 3.0
color1 = (255, 0, 0)
color2 = (168, 201, 255)
color3 = (255, 0, 0)
area1 = pi * radius1 ** 2
area2 = pi * radius2 ** 2
area3 = pi * radius3 ** 2
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(center1, radius1, area1, color1))
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(center2, radius2, area2, color2))
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(center3, radius3, area3, color3))
Tämän version löydät kokonaisuudessaan tiedostosta circle_no_oo2.py.
Ratkaisun puutteet
Homma toimii, mutta ympyröiden pinta-alojen laskennasta ja niiden tietojen tulostamisesta olisi varmaankin järkevää tehdä funktioita, ettei samanlaista koodia tarvitse kopioida. Vielä ilmeisempää on, että jos haluamme selvittää, mitkä ympyrät leikkaavat keskenään tai ovat samanvärisiä, ei yllä kuvattu numeroitujen muuttujien idea ole mielekäs. Jos ympyröitä on n kappaletta, on ympyräpareja n(n-1)/2 kappaletta ja saman verran tarvitsisimme kopioita koodista.
Tässä yhteydessä on syytä mainita ohjelmointiin ja tietotekniikkaan yleisemminkin liittyvä erittäin tärkeä juttu:
Älä kopioi koodia paikasta toiseen! Äläkä myöskään dataa!
Yksi ohjelmoinnin keskeisiä periaatteita on, että saman ohjelmakoodin kopiointia paikasta toiseen pyritään välttämään. Emme copy-pastea koodinpätkiä, emmekä myöskään käsin naputtele samaa koodia uudestaan. Syy tähän on, että jos (tai oikeammin kun) huomaamme virheen koodissa tai jostain muusta syystä haluamme muuttaa sitä, joudumme tekemään muutokset kaikkiin kopioihin erikseen. Voi olla jopa vaikea löytää kaikkia kopioita.
Vastaavasti pyrimme välttämään saman datan kopiointia tai syöttämistä moneen kertaan. Tämä ns. kertasyöttöperiaate on aivan keskeinen tietojenkäsittelyn periaate. Jos teemme datasta kopioita, joudumme huolehtimaan siitä, että muutosten sattuessa kaikki kopiot päivitetään ja tässä sattuu usein virheitä.
On kuitenkin tilanteita, joissa esimerkiksi tehokkuuden tai luotettavan saatavuuden takia sama data kannattaa tallettaa useaan eri paikkaan, esimerkiksi useaan internetissä olevaan tietokoneeseen. Tällöin vaaditaan systemaattisia menetelmiä, joilla eri kopiot saadaan pysymään sisällöltään samanlaisina.
Versio 3: Ympyröiden tiedot monessa listassa
Seuraava yritys voisi olla käyttää listoja ympyrätiedon esittämiseen ja määrittää funktiot ympyröiden liittyvien tietojen tulostamiseen sekä pinta-alojen ja leikkausten laskentaan, esimerkiksi seuraavasti:
...
centers = [(10.0, 20.0), (0.0, -5.0), (10.5, 19.5), (5.0, -5.0)]
radii = [2.5, 7.5, 3.0, 8.5]
colors = [(255, 0, 0), (168, 201, 255), (255, 0, 0), (0, 0, 0)]
def area(i):
return pi * radii[i] ** 2
def info(i):
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(centers[i], radii[i], area(i), colors[i]))
def intersects(i1, i2):
(x1, y1) = centers[i1]
(x2, y2) = centers[i2]
ds = (x2 - x1) ** 2 + (y2 - y1) ** 2
return ds <= radii[i1] ** 2 or ds <= radii[i2] ** 2
def same_color(i1, i2):
return colors[i1] == colors[i2]
for i in range(len(centers)):
info(i)
for i1 in range(len(centers)):
for i2 in range(i1 + 1, len(centers)):
if intersects(i1, i2):
print('Ympyrät {} ja {} leikkaavat'.format(i1, i2))
else:
print('Ympyrät {} ja {} eivät leikkaa'.format(i1, i2))
... # Loput koodista
Tämän version löydät kokonaisuudessaan tiedostosta circle_no_oo3.py.
Ratkaisun puutteet
Koska ympyröitä koskeva tieto on kolmessa eri listassa, vaatii uusien ympyröiden lisääminen huolellisuutta. Meidän on oltava tarkkana, että lisäämme keskipisteen, säteen ja värin eri listojen samaan indeksiin, koska muuten ympyröiden tiedot menevät sekaisin.
Versio 4: Ympyröiden tiedot monikkoina
Jotta kutakin ympyrää koskevat tiedot pysyisivät synkronoituina, voisimme yrittää koota niiden tiedot yhteen, esimerkiksi niin että yhtä ympyrää vastaisi monikko (center, radius, color).
...
circles = [((10.0, 20.0), 2.5, (255, 0, 0)),
((0.0, -5.0), 7.5, (168, 201, 255)),
((10.5, 19.5), 3.0, (255, 0, 0)),
((5.0, -5.0), 8.5, (0, 0, 0))]
def area(c):
return pi * c[1] ** 2
def info(c):
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(c[0], c[1], area(c), c[2]))
... # Loput koodista
area
ja info
saavat parametrikseen monikon, eivätkä listan indeksiä.Tämän version löydät kokonaisuudessaan tiedostosta circle_no_oo4.py.
Ratkaisun puutteet
Tämän version heikkous on siinä, että emme koodista helposti näe, mitä ympyrän ominaisuutta missäkin käsitellään. Oliko säde monikon kohdassa yksi vai kaksi? Entä väri?
Versio 5: Ympyröiden tiedot sanakirjoissa
Voisimme yrittää ratkaista tämän käyttämällä sanakirjoja.
circles = [{'center': (10.0, 20.0), 'radius': 2.5, 'color': (255, 0, 0)},
{'center': (0.0, -5.0), 'radius': 7.5, 'color': (168, 201, 255)},
{'center': (10.5, 19.5), 'radius': 3.0, 'color': (255, 0, 0)},
{'center': (5.0, -5.0), 'radius': 8.5, 'color': (0, 0, 0)}]
def area(c):
return pi * c['radius'] ** 2
def info(c):
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(c['center'], c['radius'], area(c), c['color']))
... # Loput koodista
Tämän version löydät kokonaisuudessaan tiedostosta circle_no_oo5.py.
Ratkaisun puutteet
Tätä on jo helpompi lukea. Yksi heikkous tässä kuitenkin on. Entä jos ympyrän pinta-alaa tarvitaan monessa kohtaa ohjelmassa? Pinta-ala ei muutu, joten sen voisi laskea vain kerran ja tallettaa ympyrän tietoihin myöhempää käyttöä varten.
Versio 6: Ympyröiden tiedot sanakirjoissa, talletetaan pinta-ala
Määritetään funktio store_area
, joka laskee ympyrän alan ja tallettaisi sen sanakirjaan.
... # Ympyröiden määritys kuten versiossa 5
def store_area(c):
c['area'] = pi * c['radius'] ** 2
for c in circles:
store_area(c)
def info(c):
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(c['center'], c['radius'], c['area'], c['color']))
... # Loput myös samalla tavalla kuin versiossa 5.
Tämän version löydät kokonaisuudessaan tiedostosta circle_no_oo6.py.
Ratkaisun puutteet
Tämä on jo aika siistiä, mutta jos luomme uusia ympyröitä jossain vaiheessa, pitää meidän muistaa kutsua store_area -funktiota niille. Muuten pinta-ala jää laskematta ja tallettamatta.
Versio 7: Ympyrät olioina
Entä jos voisimme jotenkin taata, että aina kun luomme ympyrän, sen pinta-ala tulisi laskettua ja talletettua? Tämä järjestyy, kun käytämme
olioita ja luokkia. Ensiksi määritämme luokan Circle
.
class Circle:
def __init__(self, center, radius, color):
self.center = center
self.radius = radius
self.color = color
self.area = pi * radius ** 2
def info(self):
print('Ympyrän keskipiste on {}, säde {} ja pinta-ala {}. Sen väri on {}'
.format(self.center, self.radius, self.area, self.color))
def intersects(self, other):
(x1, y1) = self.center
(x2, y2) = other.center
ds = (x2 - x1) ** 2 + (y2 - y1) ** 2
return ds <= self.radius ** 2 or ds <= other.radius ** 2
def same_color(self, other):
return self.color == other.color
circles = [Circle((10.0, 20.0), 2.5, (255, 0, 0)), ...]
... # Luotujen ympyröiden käyttö
__init__
suoritetaan aina kun uusi olio luodaan.self.area
.def
merkinnällä määritettyjä funktioita kutsutaan metodeiksi. Ne poikkeavat funktioista siinä, että niillä on
erityinen ensimmäinen parametri, jonka nimi (tavallisesti) on self
.intersects
ja same_color
ottavat toisena parametrinaan jonkin toisen ympyrän.Uuden ympyrän voimme luoda ja tallettaa muuttujaan y
seuraavasti: y = Circle((10.0, 20.0), 2.5, (255, 0, 0))
.
Voisimme selkeyden vuoksi kirjoittaa myös y = Circle(center=(10.0, 20.0), radius=2.5, color=(255, 0, 0))
.
Jos haluamme tulostaa ympyrään y
liittyvät tiedot, kirjoitamme y.info()
. Jos meillä on kaksi ympyrää y1
ja
y2
voimme tarkistaa ovatko ne samanväriset kutsulla same_color(y1, y2)
.
Tämän version löydät kokonaisuudessaan tiedostosta circle.py.
Nyt ympyrän luonti ja pinta-alan laskeminen tapahtuu siististi samalla kertaa.
Mihin perintää tarvitaan?
Tähän asti olemme käsitelleet vain ympyröitä. Mitä jos haluaisimme ottaa mukaan muita geometrisia muotoja, kuten neliöitä, suorakaiteita ja ellipsejä?
Tässä vaiheessa kannattaa pohtia, miten hyvin toimisivat yllä esitetyt yritykset, joissa ei käytetty olioita ja luokkia. Miten tallettaisit erilaisia muotoja käyttäen listoja? Kuinka hyvin tämä onnistuisi käyttäen useampaa listaa, kuten yllä versiossa kolme? Yhteinen ominaisuus eri kuvioilla olisi varmaankin väri. Säteen ja keskipisteen tilalta suorakaiteen (x- ja y-akselien suuntaisen) määrittävät kaksi pistettä (vasen yläkulma ja oikea alakulma); vastaavasti ellipsin piste ja kaksi puoliakselia. Pitäisikö olla omat listat suorakaiteiden, ympyröiden ja ellipsien ominaisuuksille? Toimisiko yllä esitetyn version neljä monikkoesitys paremmin? Entä sanakirjaesitys?
Kuviot olioina
Ongelma ratkeaisi melko siististi version seitsemän menetelmällä, eli että määritämme jokaiselle kuviotyypille oman luokan. Vielä siistimmin
tämä ratkeaa, jos määritämme ensin luokan Shape
, joka sisältää kaikille kuvioille yhteiset ominaisuudet.
class Shape:
def __init__(self, color):
self.color = color
def same_color(self, other):
return self.color == other.color
def info(self):
print(str(self))
def compute_area(self):
pass
str
_ kutsuu oliolle sen metodia __str__..
Jotta saadaan järkevän näköinen tulostus, on alaluokkien määritettävä uudelleen tämä metodi.compute_area
määritetään alaluokissa.Määritetään nyt luokalle Shape
alaluokkia. Havaitaan, että ympyrä on itse asiassa ellipsin erikoistapaus. Saamme seuraavan koodin:
class Ellipse(Shape):
def __init__(self, center, a, b, color):
super().__init__(color)
self.center = center
self.a = a
self.b = b
self.area = self.compute_area()
def __str__(self):
return '{}(center={}, a={}, b={}, color={})'
.format(type(self).__name__, self.center, self.a, self.b, self.color)
def compute_area(self):
return pi * self.a * self.b
class Circle(Ellipse):
def __init__(self, center, radius, color):
super().__init__(center, radius, radius, color)
self.radius = radius
def __str__(self):
return '{}(center={}, radius={}, color={})'
.format(type(self).__name__, self.center, self.radius, self.color)
compute_area
tarvitsee määrittää luokalle Ellipse
, mutta ei sen alaluokalle Circle
, koska ellipsin
kaava hoitaa asian oikein myös ympyrälle.Vastaavasti havaitsemme, että neliö on suorakaiteen erikoistapaus ja saamme seuraavan koodin:
class Rectangle(Shape):
def __init__(self, point1, point2, color):
super().__init__(color)
self.point1 = point1
self.point2 = point2
(x1, y1) = point1
(x2, y2) = point2
self.width = abs(x1 - x2)
self.height = abs(y1 - y2)
self.area = self.compute_area()
def __str__(self):
return '{}(point1={}, point2={}, color={})'
.format(type(self).__name__, self.point1, self.point2, self.color)
def compute_area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, center, side, color):
(x, y) = center
super().__init__((x - side / 2, y - side / 2), (x + side / 2, y + side / 2), color)
self.center = center
self.side = side
def __str__(self):
return '{}(center={}, side={}, color={})'
.format(type(self).__name__, self.center, self.side, self.color)
compute_area
luokalle Rectangle
, mutta ei sen alaluokalle Square
, koska suorakaiteen
kaava hoitaa asian oikein myös neliölle.Kuvioita voidaan nyt luoda ja käyttää seuraavaan tapaan:
crimson = (0xDC, 0x14, 0x3C)
white = (0xFF, 0xFF, 0xFF)
indigo = (0x4B, 0x00, 0x82)
shapes = [Circle((10.0, 20.0), 2.5, crimson),
Rectangle((-5.0, 5.0), (5.0, 5.0), white),
Ellipse((0.0, -5.0), 7.5, 6.0, indigo),
Circle((10.5, 19.5), 3.0, crimson),
Square((5.0, -5.0), 8.5, white),
Ellipse((6.0, 10.0), 7.5, 6.0, indigo),
Square((20.0, 30.0), 10.0, indigo)]
for s in shapes:
s.info()
... # Loput koodista
Tämän version löydät kokonaisuudessaan tiedostosta shape.py.
Paljon vielä puuttuu geometrisia muotoja käsittelevästä ohjelmasta, mutta hyvään alkuun on päästy. Muutama havainto vielä.
- Intersects -metodia ei ole toteutettu. Se on huomattavasti monimutkaisempi erityyppisille olioilla kuin vaikkapa pelkille ympyröille.
- Pinta-alan alustaminen joudutaan tässä tekemään alaluokkien luontimetodeissa, vaikka metodi
compute_area
on määritetty jo luokassaShape
ja olisi luontevaa kutsua sitäShape
luokan luontimetodissa. Ongelmana kuitenkin on, että alaluokat käyttävät omissacompute_area
-metodeissaan muuttujia, jotka asetetaan vastaShape
-luokan luontimetodin kutsumisen jälkeen. Voisimme toki laittaa yläluokan luontimetodin kutsun epäortodoksisesti alaluokan luontimetodien loppuun, mutta entä jos meidän tarvitsisi yläluokan luontimetodissa tehdä asioita sekä ennen alaluokan luontimetodin operaatioita että niiden jälkeen? Tähän on olemassa "taikatemppu", jota luultavasti et kovin usein tarvitse, mutta mikäli olet utelias niin katso After init metaluokan avulla.
Dynamic method dispatch
Olioiden tiedot tulostetaan yllä for
-silmukassa käyttäen listaa shapes
ja metodia info()
. Mistä Python tietää,
minkä tulostuksen metodi info
kullekin oliolle tekee? Metodihan on määritetty yläluokassa Shape
, joten sillä ei ole tietoa
minkä alaluokan ilmentymään sitä sovelletaan.
Metodin info
koodissa kutsutaan Pythonin valmiiksi määritettyä funktiota str, joka puolestaan kutsuu olion metodia __str__. Luokka Shape
ei määritä kyseistä metodia,
joten se perii sen yläluokaltaan object
. Alaluokissa kuitenkin __str__
on määritetty uudestaan ja Python ymmärtää kutsua
kullekin oliolle sen luokassa määritettyä versiota metodista __str__
. Tätä olio-ohjelmoinnin kannalta keskeistä menetelmää kutsutaan
dynaamiseksi metodin valinnaksi (eng. dynamic method dispatch). Suoritettava metodi valitaan olion tyypin mukaisesti vasta juuri ennen
kuin metodia aletaan suorittaa. Toinen vaihtoehto olisi staattinen metodin valinta. Tällöin suoritettava metodi valittaisiin ennen ohjelman
suoritusta, esimerkiksi ohjelman käännöksen aikana. Sana dynaaminen tarkoittaa tässä yhteydessä ohjelman suorituksen aikana tapahtuvaa
ja staattinen ennen ohjelman suoritusta tapahtuvaa.
Lopuksi vielä kysymys: Oletetaan, että olemme tehneet sijoituksen Circle((10.0, 20.0), 2.5, crimson)
. Mitä metodin kutsu
y.info()
tuottaisi, jos emme olisi määrittäneet metodia __str__
uudestaan luokassa Circle
?
HUOM! Tästä eteenpäin on viimevuotista materiaalia, joka muuttuu luultavasti.
Oliokäsitteet
Olio-ohjelmointiin liittyy koko joukko käsitteitä ja niitä kuvaavia termejä. Oheisessa kuvassa on hahmotettu näitä 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 identiteetti ja samuus
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 sanakirja- eli 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']
>>> kaarlo.taidot
['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.