Tapaaminen 03.12.2009

Aiheina:

  • kapselointi
  • yleistäminen
  • pintapuolinen katsaus seuraaviin aiheisiin:
    • yksikkötestaus, doctest
    • lista
    • monikko (tupla)
    • sanakirja
    • luokka, olio ja olio-ohjelmointi
    • nimiavaruus

Linkkejä:

Kapselointi ja yleistys

Kapseloinnilla tarkoitetaan ohjelmakoodin käärimistä funktioksi, jolloin saamme nimetyn työvälineen, jolla voimme suorittaa kyseisen toiminnon useammassa kohdassa ohjelmaamme. Samalla ohjelmakoodia voidaan yleistää. Siis, jos alkuperäinen ohjelmakoodimme suorittaa asiansa tietyssä erikoistapauksessa, esimerkiksi tietyllä muuttujan arvolla, voimme määritellä tämän muuttujan uuden funktiomme parametriksi. Näin funktiota voidaan käyttää ohjelmakoodin alkuperäisessä tarkoituksessa, mutta myös muissa vastaavissa tilanteissa. Toiminnan kapselointi funktioon saa aikaan myös sen, että funktiota käyttävän ei tarvitse tietää, miten funktio sisäisesti toimii. Riittää tietää, että funktio on “musta laatikko”, joka tekee sen, mitä lupaa.

Kapseloinnista on myös se hyöty, että funktion sisäisesti käyttämät muuttujat pysyvät funktion sisällä, eivätkä näy ulos päin.

Esimerkkejä kapseloinnista ja yleistämisestä.

Doctest

Doctest on testauksen muoto, jossa funktiota määriteltäessä funktion docstring:iin kirjoitetaan tärkeimpiä testattavia erikoistapauksia siinä muodossa, jollaisina funktion kutsut ja tulokset näkyisivät Pythonin interaktiivisessa komentotulkissa. Testattaessa Python suorittaa docstringissa määritellyt funktion kutsut ja vertaa tuloksia docstringissa kerrottuihin.

Esimerkiksi:

def foo(n):
    """ >>> foo(0)
        False
        >>> foo(-1)
        False
        >>> foo(1)
        True
    """

Doctestit suoritetaan esimerkiksi laittamalla seuraavat kolme riviä Python-tiedoston loppuun ja ajamalla tiedosto:

if name == '__main__';
    import doctest
    doctest.testmod()

Lista

Pythonissa on käytettävissä peräkkäisiä alkioita sisältävä, osista koostuva tietorakenne, lista. Listojen määrittelyyn käytetään hakasulkeita ja listassa olevat alkiot erotellaan pilkulla:

[0,1,2]
["foo", "bar"]

Listan alkiot voivat olla mitä tahansa käytettävissä olevaa tyyppiä eikä niiden tarvitse olla keskenään samaa tyyppiä. Listan alkioina voi olla tietenkin myös listoja. Lista voidaan sijoittaa muuttujaan.

lst = [0,1,"foo",["bar","baz",3]]

Listan alkioihin voidaan viitata indeksien avulla. Alkioiden numerointi alkaa nollasta!

>>> lst[1]
1
>>> lst[3]
["bar","baz",3]

Koska lst[3] oli lista, voimme viitata sen paikalla 1 olevaan alkioon seuraavasti:

>>> lst[3][1]
"baz"

Listan alkioihin voidaan viitata myös lopusta käsin:

>>> lst[-1]
["bar","baz",3]
>>> lst[-2]
'foo'

Alkion esiintymistä listassa voidaan testata kätevästi operaattorilla in:

>>> 3 in lst
False
>>> 3 in lst[3]
True

Tämä testi on usein hyvin käyttökelpoinen esimerkiksi if-lauseiden yhteydessä:

if 3 in lst:
    .......

Listan viipalointi

Käytetään esimerkkinä listaa foolist.

foolist = ['a','b','c','d','e','f']

Nyt tästä listasta voidaan viipaloida osia kertomalla mistä ja mihin. Esimerkiksi:

>>> foolist[1:3]
['b','c']

Miten viipaloinnin indeksit toimivat? Voidaan ajatella listan välit numeroiduksi niin, että 0 on alkion ‘a’ vasemmalla puolella, 1 alkioiden ‘a’ ja ‘b’ välissä ja niin edelleen. Nyt foolist[1:3] leikkaa siis mukaan kaiken, mikä on välien 1 ja 3 välissä. Oletusindekseinä ovat listan alku (0) ja listan loppu.

>>> foolist[:4]
['a','b','c','d']
>>> foolist[3:]
['d','e,'f']

Viipalointi palauttaa uuden listan. Tätä ja oletusindeksointia hyödyntäen lista voidaan kopioida näppärästi näin:

>>> newlist = foolist[:]
>>> newlist
['a', 'b', 'c', 'd', 'e', 'f']

Huomioitavaa on, että lista on muunnettava tyyppi, eli sen olemassa olevan listan alkioita voi vaihtaa sijoittamalla haluttuun kohtaan uuden alkion.

>>> f = [0,1,2]
>>> f[0]
0
>>> f[0] = 3
>>> f[0]
3

Monikko

Listan kanssa monessa asiassa hyvin saman tapainen käsite on monikko, joka määritellään muutoin listan tapaan, mutta käytetään kaarisulkeita.

b = (0,1,2)

Alkion kuulumista monikkoon voidaan testata in operaattorilla:

if 1 in b:
    .....

Monikon alkioihin viitataan samoin kuin listankin alkioihin:

>>> b[0]
0

Monikon tärkein ero listaan kuitenkin on se, että monikon alkioita ei voi vaihtaa. Monikko on muuttumaton.

>>> b[0] = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

Jos monikkoon laitetaan vain yksi alkio, pitää se ilmoittaa muodossa:

('a',)

Alkion perään kirjoitettu pilkku kertoo Pythonille, että kyseessä on monikko, jossa on yksi alkio, eikä pelkkä sulkeisiin kirjoitettu yksittäinen alkio.

Sanakirja

Myös aaltosulkeet ovat käytössä Pythonissa. Niillä määritellään sanakirja. Listassa ja monikossa alkiot olivat indeksien mukaan järjestyksessä ja alkioiden haku tapahtui näillä kokonaislukuindekseillä. Sanakirjassa tämä toiminto on yleistetty niin, että indeksien sijasta käytetäänkin avaimia, jotka ovat avaimeksi soveltuvia alkioita. Varsinaisiin alkioihin, eli arvoihin viitataan näillä avaimilla.

>>> foo = {}
>>> foo['one'] = 'uno'
>>> print foo['one']
uno
>>> print foo['uno']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'uno'
>>> foo['one'] = 'yksi'
>>> foo['two'] = 'kaksi'

Sanakirjan avain - arvo -parit voi määritellä myös kerralla luettelemalla seuraavasti:

foo = {'one': 'yksi', 'two': 'kaksi'}

Listat sanakirjan kaikista avaimista, kaikista arvoista ja kaikista avain-arvo-pareista saa pyytämällä:

>>> foo.keys()
['one', 'two']
>>> foo.values()
['yksi', 'kaksi']
>>> foo.items()
[('one','yksi'), ('two','kaksi')]

Avaimen esiintymistä sanakirjassa voi testata funktiolla has_key():

>>> foo.has_key('one')
True

Jos on tarvetta käydä läpi sanakirjan kaikki avain-arvo-parit esimerkiksi silmukassa, voidaan tähän käyttää iteritems()-funktiota:

for avainarvopari in foo.iteritems():
    ....

Luokat ja oliot

Ohjelmoinnissa käsitellään usein useita alkioita, joilla on yhteisiä ominaisuuksia. Esimerkiksi kilpikonnagrafiikan kilpikonnalla on ominaisuuksina muun muassa suunta ja tieto, onko sen häntä alhaalla. Ominaisuuksien lisäksi näillä alkioilla on yhteisiä toimintoja, joita ne voivat tehdä tai joita niille voidaan tehdä. Esimerkiksi kilpikonna voi kääntyä, edetä tai laskea taikka nostaa häntänsä. Tällaisia alkioita varten voidaan luoda abstrakti tietotyyppi, eli luokka, jonka esiintymiä (instansseja) yksittäiset alkiot ovat. Luokkaan kootaan siis tieto, mitä ominaisuuksia ja toimintoja kyseiseen luokkaan kuuluvilla yksittäisillä esiintymillä eli olioilla on.

Määritellään yksinkertainen luokka Point.

class Point:
    pass

Luodaan luokasta kaksi esiintymää, oliot foo ja bar.

>>> foo = Point()
>>> bar = Point()

Jos Pythonilta kysytään, mitä muuttujat foo ja bar sisältävät, kertoo Python, että kyseessä on kaksi Point-luokan esiintymää, eli instanssia. Tulostuksessa näkyvät muistipaikkojen numerot ovat erit, koska foo ja bar ovat eri olioita, eli saman luokan kaksi eri esiintymää.

>>> foo
<__main__.Point instance at 0x7f9d20009dd0>
>>> bar
<__main__.Point instance at 0x7f9d1f320758>

Olioille voidaan “lennosta” luoda ominaisuuksia:

>>> foo.x = 1.0
>>> foo.y = 3.0
>>> foo.x
1.0
>>> foo.y
3.0
>>> bar.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: Point instance has no attribute 'x'
>>> bar.y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: Point instance has no attribute 'y'

Jälkimmäisellä oliolla bar ei ollut ominaisuuksia x ja y, joten saimme virheilmoituksen, kun yritimme tutkia niitä.

Muutetaan luokkaa Point niin, että kaikilla sen esiintymillä on automaattisesti ominaisuudet x ja y. Luodaan luokalle myös yksi metodi, eli oliokohtainen funktio.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def print_coords(self):
        print '(' + str(self.x) + ',' + str(self.y) + ')'

Tehdään kaksi tämän uuden luokan esiintymää, objektit foo ja bar.

>>> foo = Point(1,2)
>>> bar = Point(3,4)

Kokeillaan metodin print_coords() toimintaa:

>>> foo.print_coords()
(1,2)
>>> bar.print_coords()
(3,4)

Metodin kutsu tietylle oliolle suorittaa siis kyseisen funktion juuri tälle oliolle. Tässä tapauksessa foo.print_coords() palautti juuri olion foo ominaisuutena olevat x- ja y-koordinaatit.

Nimiavaruuksista

Python pitää oletuksena huolta siitä, etteivät ohjelmaan tuotavien modulien sisältämät luokat, funktiot ynnä muut “tavarat” mene nimiltään ristiin. Esimerkiksi:

import point
class Point:
    pass
foo = Point()
bar = point.Point()

Tässä ohjelmaan tuodaan point-moduli, joka sisältää muun muassa Point-luokan. Lisäksi luomme myös oman Point-luokan. Nämä kaksi samannimistä luokkaa eivät kuitenkaan sotkeudu, sillä point-modulista tulevaan luokkaan viitataan merkinnällä point.Point ja itse tekemäämme luokkaan merkinnällä Point. Nämä luokat ovat siis eri nimiavaruuksissa. Toinen nimiavaruudessa point ja toinen “tässä” nimiavaruudessa.

Tämän takia kannattaa olla erityisen varovainen muotoa from moduli import * komennon kanssa, sillä se tuo kaiken modulin sisältämän tavaran “tähän” nimiavaruuteen.

Tehtävä

Tee peli, jossa on kolme vaihtoehtoa: kivi, paperi ja sakset. Paperi voittaa kiven, sakset voittavat paperin ja kivi voittaa sakset.

Toteuta peli niin, että tietokone

  1. valitsee oman valintansa
  2. kysyy pelaajan valinnan
  3. tulostaa valinnat ja voittajan

Pelissä voi käyttää hyväksi moduulia random

import random
  • Funktio random.random() palauttaa luvun väliltä 0.0 … 1.0.
  • Funktio random.choice(seq) palauttaa satunnaisen alkion jaksosta (lista, monikko,…) seq.