Ohjelmoinnin peruskurssi Y2, kurssimateriaali

1.2. Python olio-ohjelmointi

«  1.1. Kurssin esittely   ::   Etusivulle   ::   2. Kierros 2 (29.1.2016 kello 23:59)  »

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))
Värit esitetään usein ns. RGB-kolmikkoina (Red, Green, Blue), missä kunkin värikomponentin kirkkaus on välillä 0 - 255.

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
Keskipiste on monikon ensimmäisessä alkiossa, säde toisessa ja väri kolmannessa.
Funktiot 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
Kutakin ympyrää vastaa sanakirja {'center': ..., 'radius':..., 'color', ...}.

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ö
Luontimetodi __init__ suoritetaan aina kun uusi olio luodaan.
Tässä lasketaan pinta-alan valmiiksi ominaisuusmuuttujaan self.area.
Luokan sisällä def merkinnällä määritettyjä funktioita kutsutaan metodeiksi. Ne poikkeavat funktioista siinä, että niillä on erityinen ensimmäinen parametri, jonka nimi (tavallisesti) on self.
Metodit 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
Pythonin valmiiksi määritetty funktio str _ kutsuu oliolle sen metodia __str__.. Jotta saadaan järkevän näköinen tulostus, on alaluokkien määritettävä uudelleen tämä metodi.
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)
Olion o luokka saadaan kutsulla type(o) ja luokan nimi merkkijonona luokan ominaisuudesta __name__.
Metodi 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)
Tässäkin riittää määrittää metodi 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
Värit ovat yhä kolmikkoja, mutta tässä komponentit annetaan heksadesimaalinumeroina, ks. The Python Language Reference: 2.4.3. Numeric literals. Miksi Crimson, white and indigo?

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 luokassa Shape ja olisi luontevaa kutsua sitä Shape luokan luontimetodissa. Ongelmana kuitenkin on, että alaluokat käyttävät omissa compute_area -metodeissaan muuttujia, jotka asetetaan vasta Shape -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.

../_images/oliokasitteet.png

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>
Näin voidaan luoda olio, jolla ei ole mitään ominaisuuksia. Ks. class object

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, 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']
>>> 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:

  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

«  1.1. Kurssin esittely   ::   Etusivulle   ::   2. Kierros 2 (29.1.2016 kello 23:59)  »