2. Materiaali: Muodonmuutoksia ja risteyksiä¶
Ennemerkit¶
Tällä viikolla teemme Pythonista laskimen sijaan kirjoituskoneen. Opimme myös edistyneempää kommunikaatiota ohjelman ja käyttäjän välillä. Koodi alkaa reagoida siihen, mitä ihminen tekee. Koneiden vallankumous on siis aivan nurkan takana.
Viikon aiheena on jälleen keskeisiä ohjelmointikäsitteitä: ehtolauseet ja merkkijonot, joista jälkimmäiseen tehtiin pintaraapaisu jo aiemmin. Periaatteessa näillä työkaluilla voi jo tehdä jotain niinkin eeppistä kuin tekstiseikkailupelin. Me emme kuitenkaan tee tekstiseikkailua, koska sellaisen toteuttamisessa tekstin kirjoittaminen on merkittävästi suuremmassa roolissa kuin koodin. Sen sijaan mietimme läpi yleisiä ongelmaskenaarioita, joita ohjelmoinnissa tulee vastaan.
Useissa todellisissa ohjelmissa on yleensä jonkinlainen valikko, josta käyttäjä valitsee, mitä ohjelmalta haluaa. Tyypillinen ohjelma ei tee vain yhtä asiaa, vaan yleensä joukon toisiinsa liittyviä asioita. Esimerkiksi kuvankäsittelyohjelmista löytyy jos jonkinlaisia työkaluja, joita yhdistää se, että niillä kaikilla luodaan tai muokataan digitaalisia kuvia. Tämän viikon teemana ovatkin juuri valinnat. Toisaalta kysymys on siitä, miten käyttäjälle viestitään olemassaolevista valinnoista; toisaalta taas siitä, miten käyttäjältä voidaan selvittää, mitä tämä haluaa tehdä. Valintoihin liittyy tietenkin myös kolmas keskeinen kysymys: miten tietokone saadaan ohjeistettua tekemään valittu asia.
Yritys ja erehdys¶
Oppimistavoitteet: Tässä osiossa käydään läpi poikkeustilanteiden käsittelyä koodissa. Opimme uuden koodirakenteen, joka on tarkoitettu juuri tätä varten.
Viime materiaalissa uhkasimme, että päästämme jatkossa vähemmän täydelliset kädelliset sörkkimään niitä kauniisti toimivia ohjelmia, joita nähtiin materiaalin esimerkeissä sekä viikon harjoitustehtävissä. Mitä siis itse asiassa tapahtuu, jos koodissa yritetään muuttaa numeroksi jotain sellasta, joka ei näytä numerolta? Tämä voi tapahtua jo pelkästään siksi, että vitsaileva käyttäjä syöttää numerokenttään aasin. Asiaa on mukavampi käsitellä ykkösharjoitusten palloesimerkin kautta, joten palataan sen maailmaan. Koodi löytyy alta.
Anna pallon ympärysmitta: 23 cm
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/joku/kansio/pallo.py in <module>()
16 return ala, tilavuus
17
---> 18 mitattu_piiri = float(input("Anna pallon ympärysmitta: "))
19 laskettu_ala, laskettu_tilavuus = laske_pallon_ominaisuudet(mitattu_piiri)
20 print("Tilavuus:", round(laskettu_tilavuus, 4))
ValueError: could not convert string to float: '23 cm'
Nyt syötetty
merkkijono
ei näytä numerolta, koska siihen on laitettu yksikkö perään. Tästä kertoo virheviestin
viimeinen rivi. Koska virheviestit ovat tavallisille kuolevaisille pelottavuudeltaan Necronomiconin luokkaa, käyttäjien mielenterveyttä pitää suojella kiltimmän näköisillä huomautuksilla. Tähän suurten muinaisten vastaiseen toimintaan perehdytään seuraavaksi.Poikkeusten filosofia¶
Koska emme voi tietää, mitä kaikkea käyttäjät keksivät ohjelmaan
syöttää
, täytyy ohjelmassa olla jokin tapa, jolla se käsittelee poikkeustilanteet. Tätä varten on Pythonissa, ja yleisesti ohjelmointikielissä, oma rakenteensa; puhutaan virheiden tai poikkeusten
(engl. exception) käsittelystä. Perusajatuksena on, että koodia yritetään suorittaa normaalisti, mutta tiedostetaan samalla, että tiettyjä poikkeustilanteita voi syntyä, ja varaudutaan niihin. Tässäkin asiassa on kaksi puolta: ensiksi pitää selvittää, mitkä ovat mahdolliset poikkeustilanteet, ja toiseksi pitää tietää, miten niitä kohdattaessa toimitaan.
Kun kokeilimme syöttää kirjaimia ohjelmaan, saimme vastaukseksi tylyn ValueError-
viestin
. Ei ole olemassa mitään maagista keinoa, jolla selvittää kaikki poikkeustilanteet. Täytyy vain itse kokeilla kaikkea, mitä mieleen juolahtaa, ja toivoa, ettei käyttäjillä ole laajempi mielikuvitus. Yleensä uusia ongelmatilanteita putkahtelee esiin ohjelman kehityksen edetessä, joten aiemmin kirjoitettua koodia saattaa joutua korjailemaan. Ei ole mitenkään harvinaista, että uusia virheitä löytyy senkin jälkeen, kun ohjelma on valmis. Esimerkiksi bugisia pelejä julkaistaan vähän väliä, koska laadunvalvonnassa ei ole aika ja mielikuvitus riittänyt yhtä pitkälle kuin tuhansien tai miljoonien pelaajien joukolla.
Pallokoodimme kohdalla meillä on kuitenkin jo tieto siitä, että yksi selkeä ongelmatilanne on se, kun käyttäjä
syöttää
jotain muuta kuin puhtaan numeron. Voimme siis tutustua poikkeusten
käsittelyyn tämän esimerkkitapauksen kautta. Yksi hyvä tapa toimia tämän poikkeuksen kohdalla on ottaa virheviestin
antama informaatio, ja tulostaa se käyttäjälle ystävällisemmässä muodossa. Ohjelma voi siis vaikka sanoa: "Syötteessä tulee olla pelkästään numeroarvo." Tämän jälkeen ohjelma voidaan lopettaa, minkä jälkeen käyttäjä pystyy suorittamaan sen uudelleen ohjeesta viisastuneesta. Eihän tämä ihanteellinen menettely ole, mutta parempaan kykenemme vasta myöhemmin.Poikkeusten rakenne¶
Pallokoodin kyselyrivillä voi siis syntyä
poikkeus
, jos käyttäjä syöttää jotain muuta kuin float-funktiolle
kelpaavan merkkijonon
. Tästä pitäisi huomauttaa ystävällisesti käyttäjälle tulostamalla: "Syötteessä tulee olla pelkästään numeroarvo." Toteutus kiteytyy kolmeen avainsanaan
, jotka muodostavat uudenlaisen rakenteen: try, except ja else. Näistä kolmesta viimeinen ei ole pakollinen osa rakennetta mutta usein tarpeellinen. Tämän rakenteen avulla määritetään: - mitkä rivit ovat tarkastelun kohteena (try)
- mitä poikkeuksia otetaan huomioon, ja mitä niiden ilmetessä tehdään (except)
- mitä tehdään, jos suoritus onnistuu ongelmitta (else)
Meidän tapauksessamme vastaukset näihin kysymyksiin olisivat:
- input-rivi
- ValueError, kerrotaan käyttäjälle syötteen olleen vääränlainen
- suoritetaan ohjelma normaalisti loppuun
Nämä kolme sijoitetaan rakenteeseen sisennettynä:
try:
mitattu_piiri = float(input("Anna pallon ympärysmitta: "))
except ValueError:
print("Syötteessä tulee olla pelkästään numeroarvo.")
else:
laskettu_ala, laskettu_tilavuus = laske_pallon_ominaisuudet(mitattu_piiri)
print("Tilavuus:", round(laskettu_tilavuus, 4))
print("Pinta-ala:", round(laskettu_ala, 4))
Ensimmäisenä rakenteessa on siis try-
lohko
, joka aloitetaan avainsanalla
try sekä kaksoispisteellä. Varsinainen tarkistettava koodirivi, josta tiedämme poikkeustilanteen
syntyvän, on try-lohkon sisällä sisennettynä
. Seuraavana on except-lohko. Tavallisesti exceptille tulee määritellä, minkä nimisen poikkeuksen se käsittelee. Tämä määritetään kirjoittamalla poikkeuksen nimi exceptin jälkeen, ennen rivin päättävää kaksoispistettä. Sisennettynä except-lohkon sisällä on toimenpide, joka suoritetaan poikkeuksen tapahtuessa, eli tässä tapauksessa vikailmoituksen tulostaminen käyttäjälle.
Viimeisenä on else-
lohko
, jonka sisälle on sisennetty
loput ohjelman suorituksesta. Tämän lohkon sisällä oleva koodi suoritetaan vain ja ainoastaan, jos try-lohkon sisälle sijoitettu koodi suorittuu ongelmitta. Alla olevissa kahdessa animaatiossa on esitetty miten ohjelman suoritus etenee try-except-else-rakenteessa ja sen jälkeen.Kaikkia
poikkeuksia
ei ole tavallisesti tarkoitettu kiinniotettavaksi. Yleisesti ottaen oman koodin huonoudesta johtuvia ongelmia ei tulisi paikkailla virheenkäsittelyllä vaan paremmalla suunnittelulla. Poikkeuksia, jotka yleensä kertovat tämäntyyppisistä virheistä, ovat mm. TypeError ja NameError. NameError kohdattiin ohimennen ensimmäisen materiaalin tehtävässä. Se kertoo, että yritetään käyttää muuttujaa
, jota ei ole määritelty. Tämä voi johtua kirjoitusvirheestä, mutta myös siitä, että muuttuja määritetään sellaisessa osassa koodia, johon ei kaikissa tilanteissa mennä. Esimerkiksi jos tuosta yllä olevasta try-rakenteesta jätettäisiin else-osa pois: try:
mitattu_piiri = float(input("Anna pallon ympärysmitta: "))
except ValueError:
print("Syötteessä tulee olla pelkästään numeroarvo.")
laskettu_ala, laskettu_tilavuus = laske_pallon_ominaisuudet(mitattu_piiri)
print("Tilavuus:", round(laskettu_tilavuus, 4))
print("Pinta-ala:", round(laskettu_ala, 4))
Kun käyttäjä
syöttää
kyselyyn virheellisen vastauksen, tulostetaan edelleen virheilmoitus. Tulostamisen jälkeen jatketaan koodiin, joka sijaitsee nyt else-lohkon
sijaan try-rakenteen ulkopuolella. Ensimmäisellä try-rakenteen jälkeisellä rivillä yritetään lukea piiri-muuttujan
arvoa
. Sen määrittely jäi kuitenkin tekemättä, joten syntyy NameError. Määrittelyä ei ennätetä tehdä try-rakenteessa, koska try-lohkon sisällä olevan koodin suoritus katkeaa float-funktiokutsun
synnyttämään poikkeukseen
. Virhe tässä tapauksessa on tietenkin se, että kaikkien seitsemän viimeisen rivin tulisi olla else-lohkossa, koska ne voidaan suorittaa ainoastaan, jos piiri-muuttuja saadaan luotua ohjelman suorituksen alussa. Kohtalokkaat valinnat¶
Kuten alussa oli puhetta, on yleistä, että missään vähänkään laajemmassa ohjelmassa on jonkinlainen valikko. Maailman suurimpiin mysteereihin lukeutuvat imperiaaliset yksiköt, joissa ei ole niin minkäänlaista järjellistä logiikkaa. Otamme tavoitteeksi tehdä ohjelman, joka muuntaa joitain yleisimpiä hyväntuulisen-aasin-korvien-väli-keskikesällä-tyyppisiä yksiköitä SI-järjestelmään.
Valitaan siis tehtäväksi ohjelma, joka osaa muuntaa seuraavat yksiköt:
- tuuma
- jalka
- jaardi
- maili
- unssi
- pauna
- kupillinen
- pintti
- varttigallona
- gallona
- fahrenheit
Kyseessä on ensimmäinen hieman monimutkaisempi ohjelma tällä kurssilla. Tämä tarkoittaa, että pääsemme ensimmäistä kertaa oikeasti miettimään, millä tavalla ohjelma kannattaa toteuttaa. Tarvittavat laskutoimitukset ovat tiedossa, mutta lisäksi pitäisi päättää, millä tavalla ohjelmaa käytetään.
Osaamistavoitteet: Tässä osiossa opimme, miten ohjelman suoritusta voidaan ohjata riippuen käyttäjän syötteistä. Oleellisena osana tätä on ehtorakenteiden hallinta, ja tämän osion jälkeen tiedätkin, mitä ne ovat ja miten niitä käytetään valikkorakenteiden toteuttamiseen. Opit myös käskyn, joka ei tee mitään, ja miten sitä voi käyttää apuna kun hahmotellaan ohjelman rakennetta funktioilla.
Valintojen maailma¶
Ohjelmamme toteutustavan kannalta oleellinen kysymys on, miten se selvittää käyttäjän aikeet. Tässä tapauksessa siis pitäisi tavalla tai toisella selvittää, mitä yksikköä ollaan muuntamassa. Koska pystymme ainoastaan kyselemään tekstimuotoisia
syötteitä
, pääasiallisia vaihtoehtoja on kaksi: kysytään käyttäjältä, mitä yksikköä hän haluaa muuntaa, tai pyydetään sisällyttämään yksikön lyhenne syötteeseen ja tulkitaan siitä (esim. "5 oz"). Jälkimmäinen on mahdollisesti miellyttävämpi käyttää. Sen toteuttaminen vaatii kuitenkin enemmän uusia käsitteitä kuin olemme valmiit tämän materiaalin puitteissa käsittelemään. Tyydytään siis ratkaisuun, jossa käyttäjä valitsee ensin yksikön ja vasta sitten syöttää muunnettavan lukuarvon. Lisäksi, koska vaihtoehtoja on useita, jaetaan ne kategorioihin pituus, massa, tilavuus ja lämpötila. Tällöin saadaan kaksitasoinen valikko, jossa ensin valitaan kategoria ja sen jälkeen yksikkö. Lopulta käyttäjä syöttää numeroarvon ja ohjelma laskee vastaavan arvon SI-yksiköissä. Ohjelma
haarautuu
siis kuvan esittämän kaavion mukaisesti. Koska meillä on vain yksi (mutta sitäkin hämmentävämpi) lämpötilayksikkö, sen kohdalla valikko on yksitasoinen. Suunnittelun jälkeen pitäisi viimein opetella, miten ohjelman suorittamista voi tällä tapaa ohjata eri haaroihin. Tähän tarkoitukseen ohjelmointikielissä on ehtorakenteet.
Ehdottomasti ehkä¶
Ehtorakenne
on nimensä mukaisesti ehdoista koostuva rakenne, jossa kunkin ehdon toteutumisesta seuraa jotain. Ehtolauseet
muotoutuvat hyvin pitkälti luonnollisen kielen mukaisesti: "jos keskiyöllä on deadline, nyt koodataan". Aikaisemmin käsittelimme try-rakennetta, joka on tietyllä tapaa ehtorakenteen erikoistapaus: "jos kahvin keitto ei onnistu, keitä teetä; jos onnistuu, juo kahvia", jossa siis "kahvin keitto" olisi try-osa, "keitä teetä" except-osa ja "juo kahvia" else-osa. Tässä rakenteessa kuitenkin ns. ehtoina toimivat Pythonin heittämät poikkeukset
siinä, missä ehtorakenteissa keskitytään tarkastelemaan (yleensä) muuttujien
arvoja
tavalla tai toisella. Voidaan esimerkiksi tarkastella, onko käyttäjän antama syöte
identtinen "pituus"
-merkkijonon
kanssa. Koodissa tämä tapahtuisi if-lauseella: if valinta == "pituus":
Tässä valinta on siis muuttuja, johon käyttäjän syöte on tallennettu (ts. muuttuja, joka viittaa käyttäjän antamaan syötteeseen). Tällä rivillä varsinainen tarkasteltava
ehto
on puolestaan valinta == "pituus"
, joka on arvioitava lauseke. == on yhtäsuuruutta vertaileva operaattori
. Se palauttaa boolean-tyyppisen totuusarvon
, jolla on olemassa vain kaksi literaaliarvoa
: True ja False. Ohjelmoinnissa
if
-lause on lause
, joka tarkastelee sille annetun ehdon totuusarvoa: jos ehdon totuusarvo on True tai sitä vastaava arvo, suoritetaan if-lauseen alla määritelty toiminnallisuus; jos totuusarvo on False tai vastaava arvo, jätetään if-lauseen alla määritelty toiminallisuus suorittamatta. Tällöin jatketaan seuraavalta sellaiselta koodiriviltä, joka on samassa tasossa if-lauseen kanssa. Havainnollistetaan asiaa animaatioilla. Esimerkkikoodissa negatiiviset kappalemäärät sekä nolla muutetaan ykköseksi ennen kuvitteelliseen katalogiin lisäämistä.
Näissä animaatioissa oli siis kaksi tärkeää asiaa: se, miten
if-rivillä
oleva lauseke palautuu aina lopulta yksittäiseksi arvoksi
, jonka totuudellisuutta arvioidaan; ja se, miten ohjelman suoritus etenee riippuen siitä, onko ehto
tosi vai epätosi. Animaatioissa esiintyy myös uudenlainen käyttötapa print-funktiolle
, johon palataan myöhemmin. Lisäksi havaitaan, että itse if-lauseessa on käytetty uutta operaattoria
: <. Matematiikasta tuttuun tapaan kyseessä on pienempi kuin -operaattori. Alla olevaan tauluun on koottu kaikki vastaavat operaattorit, joilla vertaillaan arvoja keskenään. == | a == b | a on yhtä suuri kuin b |
!= | a != b | eri suuri kuin |
< | a < b | a on pienempi kuin b |
<= | a <= b | a on pienempi tai yhtä suuri kuin b |
> | a > b | a on suurempi kuin b |
>= | a >= b | a on suurempi tai yhtä suuri kuin b |
Huomioitavia asioita:
In [1]: 1 == 1.0
Out[1]: True
In [2]: 1 == "1"
Out[2]: False
Siinä missä kokonaisluvut ja
liukuluvut
voivat olla keskenään yhtä suuria, merkkijono
ei voi koskaan olla yhtä suuri kuin kokonaisluku tai liukuluku. Yleisesti ottaen luvut ovat ainoa tietotyyppi
, joita voidaan vertailla tällä tapaa ristiin ja saada tulokseksi True. Normaalisti erityyppisten arvojen vertailu tuottaa aina Falsen. Tämän muistaminen on tärkeää, koska käyttäjän syötteiden
vertailu if-lauseissa
voi tuottaa hämmentäviä Falseja, mikäli on unohtanut muuttaa ne luvuiksi. Tietenkin erisuuruutta vertailtaessa asetelma kääntyy toisin päin: In [1]: 1 != 1.0
Out[1]: False
In [2]: 1 != "1"
Out[2]: True
Eri tietotyyppien suuruutta ei voi vertailla muilla
operaattoreilla
:In [1]: "koirat" < 3
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-7396907f20c6> in <module>()
----> 1 "koirat" < 3
TypeError: unorderable types: str() < int()
Hämmentävää sen sijaan voi olla se, että merkkijonoja voidaan vertailla keskenään:
In [1]: "aasi" > "mursu"
Out[1]: False
In [2]: "apina" > "aasi"
Out[2]: True
Merkkijonojen "suuruus" tässä tapauksessa perustuu niiden vertailuun aakkosjärjestyksen avulla. Yleinen skenaario tässäkin on se, että kysyt käyttäjältä kaksi lukua ja vertailet niitä keskenään, mutta unohdat muuttaa ne luvuiksi jolloin:
In [1]: "31" > "4"
Out[1]: False
╯°□°)╯︵ ┻━┻
Kolmikärki¶
Yksittäinen
ehtolause
ei ole vielä kovin tehokas rakenne. Onneksi ehtorakenteisiin
saa useita haaroja. Kun katsotaan uudestaan aiemmin esitettyä kaaviota yksikkömuunnosohjelmamme toimintojen haarautumisesta, huomataan, että ylimmällä tasolla tarvittaisiin ehtorakenne, jossa on neljä eri vaihtoehtoa. Näitä vaihtoehtoja varten täytyy myös päättää, millä tavalla käyttäjä valintansa tekee. Yksinkertaisimmillaan käyttäjää voidaan pyytää kirjoittamaan "pituus", "tilavuus", "massa" tai "lämpötila", mikä on ainakin hyvin yksiselitteistä. Aloitetaan tällä ratkaisulla. Hyvä tapa luoda valikkorakenne on käyttää
funktioita
apuna. Jokainen ohjelman osa saa oman funktionsa, ja päävalikossa ainoastaan kutsutaan
näitä funktioita. Tällä tavalla itse valikkokoodi jää hyvin selkeäksi. Vastaavasti kaikki ohjelman osatkin löytyvät omista funktioistaan, missä niitä on helpompi muokata kuin osana pääohjelmaa
, johon sijoitamme ehtorakenteemme. Aivan erityisesti ehtorakenne on helpompi hahmottaa, jos yhden ehdon sisällä ei ole kovin montaa riviä koodia. Aloitetaan siis ohjelmamme suunnittelu siten, että luodaan jokaiselle osaohjelmalla oma funktionsa, noudattaen alkuperäistä haarautumiskaaviota: def pituus():
def massa():
def tilavuus():
def lampotila():
Tällä kertaa
funktioilla
ei ole lainkaan parametrejä
. Tätä tapahtuu erityisesti juuri valikkomaisissa ratkaisuissa, koska mitään eteenpäin siirrettävää tietoa ei vielä ole. Virheetön tämä ratkaisu tosin ei ole, sillä yllä olevan koodin suorittamisesta syntyy IndentationError. Kaksoispistettä onkin aina seurattava vähintään yksi sisennetty
rivi. Välillä on silti mukava pystyä koodin jäsentämiseksi kirjoittamaan funktiomäärittelyt valmiiksi ennen kuin miettii yhtään niiden sisällöstä. Tätä varten löytyy onneksi Pythonista hyödyllinen ominaisuus: käsky, joka ei tee yhtään mitään, eli pass
. Käytämme sitä väliaikaissisältönä funktioissa, jotta koodi voidaan suorittaa, vaikka funktiot eivät ole vielä valmiita.def pituus():
pass
def massa:()
pass
def tilavuus():
pass
def lampotila():
pass
Ehtorakenteisiin
voidaan lisätä uusia haaroja käyttämällä elif
-lausetta (sanoista else if). Nämä lauseet tulevat samalle sisennystasolle
kuin rakenteen aloittanut if-lause
ja toimivat täsmälleen samalla tavalla. Ehtorakenteessa elif-lauseen koodilohko
voidaan kuitenkin suorittaa vain, jos mikään sitä edeltänyt (eli yläpuolella oleva) osa ehtorakenteesta ei ole toteutunut. Toisin sanoen tämä tarkoittaa, että heti, kun ehtorakenteen yhdenkin haaran ehto
toteutuu, loppuja ehtoja ei edes tarkastella.luku = int(input("Anna kokonaisluku: "))
if luku < 10:
print("Luku on pienempi kuin 10")
elif 10 <= luku < 100:
print("Luku on pienempi kuin 100")
elif 100 <= luku < 1000:
print("Luku on pienempi kuin 1000")
Pintapuolisesti tämä näyttää täysin järkevältä. Kuitenkin kun muistetaan, että elif sisältää oletuksen, että sitä edeltävät ehdot eivät ole toteutuneet, havaitaan, että kummallakin esimerkin elif-rivillä ensimmäinen vertailu on turha.
- Ensimmäisessä elif-lauseessaluku on jo osoitettu olevan 10 tai suurempi, koska ensimmäinenehtoei toteutunut.
- Toisessa elif-lauseessa on jo osoitettu edellisen ehdon perusteella, että luku on 100 tai suurempi.
Tämä perustuu siihen, että kummassakaan elif-lauseessa ei oltaisi, jos luku olisi ollut pienempi kuin 10, eikä toisessa oltaisi, jos luku olisi ollut pienempi kuin 100. Esimerkki voidaankin siis kirjoittaa tämän tiedon valossa seuraavasti:
luku = int(input("Anna kokonaisluku: "))
if luku < 10:
print("Luku on pienempi kuin 10")
elif luku < 100:
print("Luku on pienempi kuin 100")
elif luku < 1000:
print("Luku on pienempi kuin 1000")
Nyt kun tiedämme, miltä useita haaroja sisältävä
ehtorakenne
näyttää elif-lauseita
käyttäen, voimme hahmotella päävalikkokoodin:valinta = input("Tee valintasi: ")
if valinta == "pituus":
pituus()
elif valinta == "massa":
massa()
elif valinta == "tilavuus":
tilavuus()
elif valinta == "lämpötila":
lampotila()
Valikon rakenne on hyvin selkeä. Ehtorakenne ohjaa ohjelman suorituksen eri toiminnoista vastaaviin
funktioihin
sen perusteella, mitä käyttäjä kirjoittaa syötteeksi
. Koska funktiot on määritelty (vaikka ne eivät mitään teekään), tämä ohjelma voidaan suorittaa. Tällä tavalla ohjelman koodaamisen voi osittaa hyvin pieniin palasiin ja joka käänteessä pystyy aina tarkistamaan, onko koodiin jäänyt virheitä. Funktioihin voisi jopa kirjoittaa jotain tämän tapaista:def pituus():
print("Valittiin pituus")
Ohjelmaa ajamalla voidaan nyt tulosteesta nähdä, että valitsemalla "pituus" ohjelma todellakin etenee pituus-funktioon. Tämä on erittäin hyvä tapa etsiä virheitä
ehtorakenteista
, joissa on monimutkaisia ja virheherkkiä ehtoja
. Mihin sitten tarvitaan juuri
elif-lausetta
? Eikö riittäisi, että käyttää montaa if-lausetta? Ero on siinä, että if aloittaa uuden ehtorakenteen siinä, missä elif jatkaa ehtorakennetta. Jokaisesta ehtorakenteesta suoritetaan korkeintaan yksi haara, ja ehtoja tarkistetaan vain siihen haaraan asti, joka suoritetaan. Jos siis valikkoesimerkissämme käyttäjä valitsee "pituus", muita ehtoja ei tarvitse tarkistaa. Jos sen sijaan olisimme tehneet valikon pelkillä if-lauseilla, kaikki ehdot tarkistettaisiin aina, koska siinä olisi yhden neljähaaraisen ehtorakenteen sijaan neljä yksihaaraista. Tässä kyseisessä tapauksessa toiminnassa ei tapahdu eroa, koska ehdoissa ei ole päällekkäisyyttä (merkkijono ei voi olla samaan aikaan yhtäsuuri kuin "pituus" ja "massa"). Päällekkäisyyttä sisältävät ehdot demonstroivat eron paremmin: Käyttökokemus on ehdoton¶
Meillä on nyt siis koodi, joka määrittelee ohjelmamme aloitusvalikon
ehtorakenteen
avulla. Kovin käytettävä se ei ole. Käyttäjä ei mm. tiedä, mitä vaihtoehtoja ohjelmassa on. Tämä voidaan korjata kirjoittamalla hieman ohjeita ennen syötteen
kysymistä. Lisätään siis tulostusrivejä pääohjelman
alkuun: print("Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi")
print("Mahdolliset toiminnot:")
print("pituus")
print("massa")
print("tilavuus")
print("lämpötila")
print()
valinta = input("Tee valintasi: ")
if valinta == "pituus":
pituus()
elif valinta == "massa":
massa()
elif valinta == "tilavuus":
tilavuus()
elif valinta == "lämpötila":
lampotila()
Viimeinen print-
kutsu
, jolla ei ole argumentteja
, tuottaa yhden tyhjän rivin. Tyhjät rivit palvelevat koodissa sen selkeyttämistä, ja sama pätee myös käyttäjälle näytettävään informaatioon. Entä jos käyttäjä syöttää tahallisesti tai vahingossa jotain, mikä ei kuulu
ehtorakenteessa
määriteltyihin vaihtoehtoihin
? Tällä hetkellä ohjelma vain palauttaa käyttäjän komentoriville
sanomatta mitään. Olisi parempi, jos ohjelma osaisi todeta: "Valitsemaasi toimintoa ei ole olemassa". Tähän tarkoitukseen sopii ehtorakenteiden kolmas komponentti, eli else
. Else kattaa kaikki tilanteet, joita sitä edeltäneet haarat eivät kata, ja sen tulee olla aina ehtorakenteessa viimeisenä (mutta se ei ole pakollinen). Lisätään se siis ehtorakenteemme loppuun: if valinta == "pituus":
pituus()
elif valinta == "massa":
massa()
elif valinta == "tilavuus":
tilavuus()
elif valinta == "lämpötila":
lampotila()
else:
print("Valitsemaasi toimintoa ei ole olemassa")
Else on ehtorakenteen yksinkertaisin osa, koska sille ei ole mahdollista määritellä
ehtoa
. Kaikessa yksinkertaisuudessaan else kattaa "kaikki muut tapaukset". Nyt ohjelma on varsin ystävällinen. Se antaa ohjeita ja osaa myös sanoa jotain, kun asiat menevät pieleen. Kokeillaan!
Merkillistä metodologiaa¶
Törmäsimme ongelmaan: jos käyttäjä antaa periaatteessa oikean komennon, mutta aloittaa sen isolla alkukirjaimella, ohjelma ei tunnista komentoa oikein. Tässä voisi tietysti todeta: "Tyhmä käyttäjä, et vain osaa". Kuitenkin, jos mahdollista, kannattaa hyväksyä myös muunlaiset kirjoitusasut sallituille sanoille. Ongelman ydin on siinä, että isot ja pienet kirjaimet ovat eri symboleita keskenään:
In [1]: "A" == "a"
Out[1]: False
Yksi idea olisi lisätä ehto, joka tunnistaa
merkkijonon
"Pituus". Entä jos käyttäjällä on caps lock päällä, ja hän syöttääkin
"PITUUS" tai "pITUUS"? Olisi kenties parempi, jos vertailusta saataisiin sellainen, että kirjainten koolla ei ole merkitystä. Tämän toteuttamiseksi voimme muuttaa käyttäjän syötteen kaikki kirjaimet pieniksi. Tähän operaatioon tarvitaan metodia
. Oppimistavoitteet: Tässä osiossa opitaan, mitä ovat metodit ja mitä niillä voi tehdä merkkijonojen tapauksessa.
Hei, olen metodi¶
Metodeja (eng. method) kutsutaan myös jäsenfunktioiksi. Metodit ovat nimittäin myös
funktioita
. Ne eroavat aiemmin käytetyistä funktioista siinä, että metodi on kiinnitetty objektiin
ts. se on jonkin objektin jäsen eli jäsenfunktio. Objekti (eng. object) tarkoittaa Pythonin kontekstissa mitä tahansa arvoa
. Käytämme kuitenkin sanaa objekti, koska on helpompaa mieltää, että jollakin objektilla on ominaisuuksia
, kuin että arvolla on ominaisuuksia. Objektista käytetään myös "oikeampaa" suomennosta olio. Mitä tämä kaikki sitten käytännössä tarkoittaa? Otetaan esimerkki:luku = round(luku)
Useimmille funktioille määritellään objekti, jota ne käsittelevät,
argumenttien
kautta. Tässä esimerkissä round-funktio käsittelee objektia, joka on luku-muuttujassa
(joka on siis sama asia kuin luku-muuttujan arvo). Metodi eroaa tästä siten, että metodi käsittelee sitä objektia, jonka jäsen se on. Alla on käytetty malliksi tulkissa
metodia strip
. Esimerkkiä ei tarvitse vielä täysin ymmärtää, mutta voit jo kokeilla sitä itse!In [1]: otus = " aasi "
In [2]: otus.strip()
Out[2]: 'aasi'
Objektin
metodit
riippuvat sen tietotyypistä
. Siispä esim. kaikilla merkkijonoilla
on samat metodit. Lista niistä löytyy Pythonin dokumentaatiosta. Vaihtoehtoisesti ne näkee myös tulkissa dir
-funktiolla
, joka listaa objektin kaikki ominaisuudet
(attribute), joihin metodit kuuluvat. Sen voi tehdä esimerkiksi antamalla dir-funktiolle argumentiksi
tyhjän merkkijonon:In [1]: dir("")
Out[1]:
['__add__',
'__class__',
'__contains__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getitem__',
'__getnewargs__',
'__gt__',
'__hash__',
'__init__',
'__iter__',
'__le__',
'__len__',
'__lt__',
'__mod__',
'__mul__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__rmod__',
'__rmul__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'capitalize',
'casefold',
'center',
'count',
'encode',
'endswith',
'expandtabs',
'find',
'format',
'format_map',
'index',
'isalnum',
'isalpha',
'isdecimal',
'isdigit',
'isidentifier',
'islower',
'isnumeric',
'isprintable',
'isspace',
'istitle',
'isupper',
'join',
'ljust',
'lower',
'lstrip',
'maketrans',
'partition',
'replace',
'rfind',
'rindex',
'rjust',
'rpartition',
'rsplit',
'rstrip',
'split',
'splitlines',
'startswith',
'strip',
'swapcase',
'title',
'translate',
'upper',
'zfill']
Funktio palauttaa listan ominaisuuksia. Alkupään ominaisuudet, joiden nimiä koristavat alaviivat, on tarkoitettu ainoastaan objektin sisäiseen käyttöön, ja niitä käytännössä käsittelee vain Python itse. Niistä ei siis tarvitse välittää. Loput ovat (tässä tapauksessa) metodeja, jotka ovat kiinnostuksemme kohteena. Kuten funktioilla, myös niillä on enemmän tai vähemmän kuvaavat nimet. Otetaan metodien käytöstä toinen yksinkertainen esimerkki, joka sattumoisin myös ratkaisee ongelmamme:
In [1]: sana = "AaSi"
In [2]: sana.lower()
Out[2]: 'aasi'
Esimerkin voisi myös lyhentää muotoon
"AaSi".lower()
. Metodikutsujen
tekeminen literaaliarvoille
on kuitenkin harvinaisempaa, koska metodin tekemän asian voisi myös tehdä literaaliarvolle käsin. Yleensä siis metodikutsuja näkee nimenomaan muuttujien
perässä. Piste erottaa objektin
ja metodin
toisistaan. Metodikutsun ensimmäinen osa siis kertoo, mistä objektista kutsuttava metodi löytyy, ja pisteen jälkeinen osa varsinaisen metodin nimen. Huomattavaa on, että esimerkissä metodikutsun sulut ovat tyhjät. Tämä on seurausta juuri siitä, että metodi käsittelee objektia johon se kuuluu. Se informaatio, joka funktiokutsussa olisi sulkujen sisällä, on nyt pisteen vasemmalla puolella. Tämä ei tarkoita, etteikö olisi metodeja joille annetaan
argumentteja
– onhan meillä funktioitakin, joille kerrotaan enemmän kuin yksi käsiteltävä arvo. Otetaan kolmantena esimerkkinä
count
-metodi
, jolla voidaan laskea, montako kertaa lyhyempi merkkijono
esiintyy toisen, pidemmän merkkijonon sisällä. Metodin toimintaa voi tarkastella Pythonin dokumentaatiosta tai sitten tulkissa
:In [1]: help("".count)
Help on built-in function count:
count(...) method of builtins.str instance
S.count(sub[, start[, end]]) -> int
Return the number of non-overlapping occurrences of substring sub in
string S[start:end]. Optional arguments start and end are
interpreted as in slice notation.
Rivillä, joka näyttää miten metodia käytetään, on ennen pistettä S. Tähän paikalle tulee siis aina pidempi merkkijono, josta toista lyhyempää merkkijonoa etsitään. Suluissa puolestaan on pakollinen
argumentti
sub sekä valinnaiset start ja end (hakasulut siis kertoivat valinnaisuudesta). Näistä sub on merkkijono, jota etsitään S-merkkijonon sisältä. Valinnaisilla argumenteilla voidaan rajata etsintä jollekin tietylle välille S-merkkijonossa. Tällä metodilla voidaan mm. laskea montako
vertailuoperaattoria
kooditiedostosta
löytyy, tai löytyykö koodista oikea määrä elif-lauseita ehtorakenteista
. Tätä ja muita koodin tarkastelua kutsutaan staattiseksi tarkistamiseksi, jossa koodia luetaan tekstinä suorittamisen sijaan. Kyseessä on tietenkin vain yksi käyttötarkoitus merkkijonojen
tutkimiselle. Tekstin tulkitseminen tietokoneella on merkittävä osa-alue tietotekniikkaa, ja myös sen voi aloittaa näin yksinkertaisilla tempuilla:teksti = input("Kirjoita tähän runo: ")
aasit = teksti.count("aasi")
print("Tekstistä löytyi", aasit, "aasi(a)")
Ketjujonottaja¶
Merkkijonometodeja
voi myös ketjuttaa. Tämä perustuu siihen, että merkkijonometodi palauttaa
muutetun kopion merkkijonosta
. Kun riviä siis suoritetaan, metodikutsun
tilalle voidaan ajatella sen palauttama arvo
. Merkkijonometodi ei siis koskaan muuta alkuperäistä merkkijonoa. Tähän on syynä se, että merkkijono on muuntumaton
(immutable) tietotyyppi
, minkä merkitys avautuu paremmin kun kohtaamme ensimmäisen tietotyypin, joka on muuntuva. Selvennykseksi katsotaan animaatiota, josta ilmenee miten merkkijonometodit, muuttujat
ja niiden arvot reagoivat keskenään. Samasta animaatiosta nähdään myös, että
metodikutsun
paikalla on pölyn laskeuduttua merkkijono
. Yksi aika yleinen metodi
, jota käytetään usein ketjutettuna, on strip. Otetaan esimerkkiskenaario, jossa käyttäjän antamaan lukuun halutaan lisätä etunollia, jotta kaikki syötetyt luvut ovat tasan neljän merkin mittaisia. Metodi, jolla etunollia voidaan lisätä, on zfill. Sille annetaan argumentiksi
pituus, joka merkkijonolla tulee olla etunollien lisäämisen jälkeen. Esimerkkikoodi näyttäisi siis tältä:luku = input("Anna numero (1-9999): ")
luku = luku.zfill(4)
print(luku)
Normaalisti tämä toimii aivan hyvin:
Anna numero (1-9999): 42 0042
Entä jos käyttäjä sisällyttää vahingossa syötteeseen välilyöntejä ennen ja/tai jälkeen numeron kirjoittamisen? (Tässä tapauksessa molemmat, voit maalata tekstin nähdäksesi numeron jälkeisen tyhjän)
Anna numero (1-9999): 42 42
Juuri tähän sopii strip-
metodi
. Se poistaa oletuksena kaikki tyhjät merkit merkkijonon
alusta ja lopusta. Tyhjiä merkkejä ovat välilyöntien lisäksi rivinvaihdot ja sarkainmerkit. Sille voidaan haluttaessa määrittää myös jokin muu merkki poistettavaksi, mutta tyhjien poisto on ehdottomasti yleisin käyttötapa. Metodilla on myös sukulaiset lstrip ja rstrip, jotka toimivat vastaavasti, mutta poistaen merkit vain alusta (lstrip, left) tai lopusta (rstrip, right). Tätä metodia tulee käyttää ennen zfill-metodia, joten koodi muuttuu seuraavanlaiseksi: luku = input("Anna numero (1-9999): ")
luku = luku.strip().zfill(4)
print(luku)
Rivin eteneminen on purettu auki seuraavassa animaatiossa, käyttäen luku-
muuttujalle
arvoa
" 42 ":Palataan viimein asiaan, mistä koko
metodihullutus
sai alkunsa. Ongelmanamme oli siis, että "pituus" ja "Pituus" ovat eri asioita Pythonin mielestä. Etsimme jo metodin, jolla tämän ongelman voi ratkaista: lower, joka muuttaa merkkijonosta
kaikki merkit pieniksi. Löysimme myös metodin, jolla syötteestä saa tyhjät vahinkomerkit pois. Luonnollisesti metodikutsuja
voi ketjuttaa myös tavallisten funktiokutsujen
perään, jotka palauttavat merkkijonon. Yksi sellainen funktio
on input. Ratkaistaan ongelma tekemällä input-rivistä seuraavanlainen: valinta = input("Tee valintasi: ").strip().lower()
Koska käyttäjän
syötteellä
ei ole muuta käyttöä ohjelmassa, tämä on paras paikka muokata sitä. Tällöin käsittelyä ei tarvitse tehdä useassa kohtaa eikä myöskään tarvita erillistä riviä sitä varten. Koko pääohjelma
näyttää nyt siis tältä:print("Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi")
print("Mahdolliset toiminnot:")
print("pituus")
print("massa")
print("tilavuus")
print("lämpötila")
print()
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "pituus":
pituus()
elif valinta == "massa":
massa()
elif valinta == "tilavuus":
tilavuus()
elif valinta == "lämpötila":
lampotila()
else:
print("Valitsemaasi toimintoa ei ole olemassa")
Operaatio logiikka¶
Jos rehellisiä ollaan, kokonaisten sanojen kirjoittaminen valikossa navigoinnin vuoksi ei ole ihmisten hommaa. Nykymaailmassa tietenkin valikoita selataan lähinnä hiirellä tai kosketusnäytöllä, mutta jo silloin kun ohjelmoitiin nuotion äärellä luolissa, osattiin tehdä valikoista inhimillisempiä. Tähän on tyypillisesti ollut kaksi tapaa: joko vaihtoehdot on numeroitu, ja valinta tehdään antamalla numero; tai jokaista vaihtoehtoa esittää jokin kirjain (yleensä valintasanan ensimmäinen, jos mahdollista). Numerointitapa johtaisi jotakuinkin tämän näköiseen
käyttöliitymään
:Mahdolliset toiminnot: 1 - Pituus 2 - Massa 3 - Tilavuus 4 - Lämpötila Tee valintasi (1-4):
Vastaavasti kirjainlyhenteiden käytöllä saataisiin jotain tämän näköistä:
Mahdolliset toiminnot: (P)ituus (M)assa (T)ilavuus (L)ämpötila Tee valintasi:
Suluissa oleva kirjain siis kertoo millä merkillä toiminnon voi valita. Valitsemme tällä kertaa jälkimmäisen tavan. Tämä aiheuttaa muutoksia tietenkin ohjeiden tulostukseen sekä itse
ehtorakenteeseen
, jossa niitä käsitellään. Aiomme itse asiassa olla niinkin huomaavaisia käyttäjää kohtaan, että ehtorakenne ymmärtää sekä kirjainlyhenteet että kokonaiset sanat. Kykenemme tähän
loogisten operaattoreiden
avulla. Loogisten operaattorien avulla voidaan yhdistää ja kääntää olemassaolevia ehtoja
. Niitä on kokonaiset kolme kappaletta: and
, not
ja or
. Ne ovat aivan samanlaisia operaattoreita
kuin aiemmatkin (esim. + ja >=) mutta sattuvat olemaan sanoja merkkiyhdistelmien sijaan. Useissa kielissä nämäkin operaattorit esitetään merkkiyhdistelmin: &&, ! ja ||, mutta Pythonin käyttämät sanat ovat kuvaavampia. Loogisia operaattoreita käytetään pääasiassa ehtolauseissa
, kun halutaan yhdistellä useita ehtoja tai esittää jokin ehto käänteisenä. Käsitellään ensimmäisenä not-operaattori, joka on looginen negaatio. Ehdottomasti yleisin käyttö tälle operaattorille on tilanne, jossa halutaan tarkistaa, onko käyttäjän antama
syöte
kokonaan tyhjä. Tyhjä merkkijono
tarkoittaa merkkijonoa, jossa on nolla merkkiä eli sen pituus on nolla. Tyhjiä merkkijonoja ovat siis ""
ja ''
, ei mikään muu. Esimerkiksi " "
ei ole tyhjä, koska sen sisältönä on yksi välilyönti, ja sen pituus on yksi. Tyhjää merkkijonoa voisi testata tietysti näin: if valinta == "":
Toinen vaihtoehto on kuitenkin:
if not valinta:
Tämän rivin toiminta perustuu siihen, että tyhjän merkkijonon
totuusarvo
on epätosi eli False. Looginen negaatio puolestaan kääntää ehdon totuusarvon käänteiseksi: se palauttaa True, jos ehto on epätosi, ja False, jos ehto on tosi. Huomattavaa operaattorissa
on se, että sillä on vain yksi operandi
toisin kuin useimmilla operaattoreilla, ja tämä operandi on aina not-operaattorin oikealla puolella. Periaatteessa se siis toimii samalla tavalla kuin miinusmerkki numeron edessä.Kaksi muuta
loogista operaattoria
, eli and ja or, ovat useammin käytössä, koska niiden avulla voidaan yhdistää kaksi ehtoa
. Yhdistystapa riippuu valitusta operaattorista: and palauttaa Truen, jos molemmat ehdot ovat tosia; or:lle riittää, että toinen ehdoista on. Meidän tarpeisiimme sopii or-operaattori. Sen toimintaa voi tutkia myös ehtolauseiden ulkopuolella tulkissa: In [1]: valinta = input("Tee valintasi: ").strip().lower()
Tee valintasi: P
In [2]: valinta == "p" or valinta == "pituus"
Out[2]: True
Jos yllä olisi käytetty or-operaattorin sijaan and-operaattoria, olisi saatu aikaan ehto, joka ei voi koskaan olla tosi. Tämä tietenkin siksi, että
merkkijono
ei voi olla samaan aikaan sekä "p" että "pituus". And-operaattoria käytetäänkin useammin tilanteissa, joissa joko tarkastellaan kahta eri muuttujaa
samanaikaisesti, tai yhtä muuttujaa kahdella eri tavalla. Esimerkiksi meillä voi olla tarkistus koordinaatistoon liittyvässä tehtävässä, jossa selvitetään onko annettu piste origo:if x == 0 and y == 0:
Vielä varoituksen sana seuraavan näköisestä rivistä, joka sisältää yleisen virheen:
if valinta == "p" or "pituus":
Luettuna sellaisenaan, englanniksi, tämä rivi voi näyttää järkevältä. Tässä kuitenkin or-operaattorin operandit ovat
valinta == "p"
ja "pituus"
. Animaatioesimerkin mukaisesti, jos käyttäjän valinta on "m", ensimmäinen ehto
on False, koska "m" ei selkeästikään ole "p". Kuitenkin toinen operandi
, joka on siis pelkästään merkkijono
"pituus", on arvoltaan aina tosi, koska se ei ole tyhjä merkkijono. Yhtäsuuruuta vertaileva ==-operaattori
jää siis täysin or-operaattorin vasemmalle puolelle, eikä or-operaattorin oikealla puolella sitä ole oikeastaan enää edes olemassa. Toisin sanoen tämä ehto on aina tosi, riippumatta siitä, mitä käyttäjä syöttää
. Vaikka siis tuntuu tyhmältä kirjoittaa valinta-muuttuja kahdesti riville, mitään oikopolkua ei vielä ole käytössämme.Tällä hetkellä
pääohjelma
näyttää siis tältä: print("Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi")
print("Mahdolliset toiminnot:")
print("(P)ituus")
print("(M)assa")
print("(T)ilavuus")
print("(L)ämpotila")
print()
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "p" or valinta == "pituus":
pituus()
elif valinta == "m" or valinta == "massa":
massa()
elif valinta == "t" or valinta == "tilavuus":
tilavuus()
elif valinta == "l" or valinta == "lämpötila":
lampotila()
else:
print("Valitsemaasi toimintoa ei ole olemassa")
Pythonin muotoiluakatemia¶
Otetaan aiemmin edistelty kaaviokuva ohjelman haarautumisesta uudelleen tarkasteltavaksi:
Kuvassa sinisellä ympyröityä osaa varten meillä onkin jo koodissa
funktion
tynkä valmiina. Tällä tynkäfunktiolla
testattiin, että pääohjelman
toteuttama valikko toimii oikein. def pituus():
print("Valittiin pituus")
Tästä tynkäfunktiosta olisi nyt tarkoitus tehdä vahva itsenäinen funktio. Funktion pitäisi kysyä käyttäjältä kaksi asiaa: mitta-arvo ja mittayksikkö. Etukäteen päätimme, että mahdolliset yksiköt ovat tuuma, jalka, jaardi ja maili. Näiden kahden asian kysymisen jälkeen funktion pitäisi laskea annettujen tietojen perusteella vastaava arvo SI-järjestelmässä. Tuumat ja jalat voisi muuttaa senteiksi, jaardit metreiksi ja mailit kilometreiksi. Jos yksiköitä vaihdellaan tällä tavalla kyselemättä, on tietenkin kohteliasta ilmoittaa yksikkö myös tuloksessa. Ohjelma voisi edetä esim. näin:
Syötä muutettava arvo: 12.4 Syötä muutettava yksikkö: tuuma 12.40" on 31.50 cm
Aiemman mallin mukainen ratkaisu olisi toteuttaa jälleen viisihaarainen
ehtorakenne
(4 ehtohaaraa
+ else virheelliselle yksikölle), ja sisällyttää jokaiseen laskutoimenpide sekä tulostus. Tällä kertaa ei tehdä haarojen toiminnallisuudelle erillisiä funktioita
, vaan sisällytetään ne suoraan pituus-funktion koodiin. Aloitetaan toteutus jälleen tynkäversiolla, josta puuttuu tulostuksesta ohjeet ja ylimääräiset hienostelut. Runko on jo pääohjelmasta
tuttu. Jokainen muunnos on yleiseltä luonteeltaan arvo * kerroin
, ja kertoimet on haettu wikipediastadef pituus():
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "tuuma":
print(arvo * 2.54)
elif yksikko == "jalka":
print(arvo * 30.48)
elif yksikko == "jaardi":
print(arvo * 0.9144)
elif yksikko == "maili":
print(arvo * 1.609344)
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Tästä päästäänkin varsinaiseen aiheeseen eli lopputuloksen kaunistamiseen.
Oppimistavoitteet: Tässä osiossa opitaan, että jotkut merkit merkkijonoissa ovat ongelmallisia. Toisena asiana tulee kauniiden tulostusten tuottaminen format-metodin avulla. Pyöristyskin saa uusia piirteitä. Tämän osuuden jälkeen hallinnassa ovat perusteet tulostaa vaikka minkälaisia merkkijonoja.
Pakokauhua¶
Yksikön valinta voi tapahtua sen lyhenteen/merkin pohjalta. Tässä tapauksessa siis in/", ft/', yd ja mi. Lähdetään siis liikkeelle siitä, että vaihdetaan nämä yllä esitettyihin
ehtolauseisiin
. Tuuman ja jalan kohdalla käytetään or-operaattoria
, jotta molemmat merkintätavat saadaan mukaan. def pituus():
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "in" or yksikko == """:
print(arvo * 2.54)
elif yksikko == "ft" or yksikko == "'":
print(arvo * 30.48)
elif yksikko == "yd":
print(arvo * 0.9144)
elif yksikko == "mi":
print(arvo * 1.609344)
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Syntaksivärjäys yllä olevassa koodissa näyttää kuitenkin oudolta. Jos "-merkki aloittaa merkkijonon, ja toinen samanlainen lopettaa sen, miten tämä
"""
pitäisi tulkita? Kooditiedoston suorittaminen antaa mysteerivirheen
File "muuntaja.py", line 42 print("Valitsemaasi toimintoa ei ole olemassa") ^ SyntaxError: EOF while scanning triple-quoted string literal
Triple-quoted string literal tarkoittaa
merkkijonoa
, joka on yksittäisten lainausmerkkien sijaan rajattu kolmella. Eli tosiasiassa myös """-merkintä aloittaa merkkijonon. Koska tiedostosta ei löydy sille paria, Python kohtaa tiedoston lopun ennen sulkevaa """-merkintää, ja heittää SyntaxErrorin auki jääneestä merkkijonosta. """ on siitä erilainen, että toisin kuin yksittäisten lainausmerkkien rajaamat merkkijonot, sen rajaamat merkkijonot saavat jatkua useita rivejä:runo = """aasisvengaa
dibadii dabadaa
svengaa vaan"""
Hienoa, mutta tämä ei varsinaisesti ratkaise ongelmaa. "-merkin esiintyminen "-merkeillä rajatun merkkijonon sisällä on ongelmallista. Asia voidaan tietenkin väistää vaihtamalla rajausmerkeiksi '. Silloin puolestaan '-merkit merkkijonon sisällä muuttuvat ongelmallisiksi. Joissain merkkijonoissa esiintyy kuitenkin sekä " että '. Mitäs sitten tehdään? Silloin
paetaan
. Ei silti nousta koneen ääreltä ja juosta auringonlaskuun, vaan käytetään pakomerkkiä (escape character), joka Pythonissa on \. Tällä merkillä muutetaan
merkkijonossa
esiintyvän merkin tulkintatapaa. Paettu " eli \" tulkitaan merkkijonon rajausmerkin sijaan pelkäksi "-merkiksi. Merkintätapa näyttää tältä: In [1]: lainausmerkki = "\""
In [2]: print(lainausmerkki)
"
\-merkin jälkeen tuleva merkki (tai merkit joissain tapauksissa) tulkitaan siis poikkeavasti. Lisätään siis kyseinen pätkä koodiimme:
def pituus():
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "in" or yksikko == "\"":
print(arvo * 2.54)
elif yksikko == "ft" or yksikko == "'":
print(arvo * 30.48)
elif yksikko == "yd":
print(arvo * 0.9144)
elif yksikko == "mi":
print(arvo * 1.609344)
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Nyt meillä on kasassa ohjelmakokonaisuus, jolla voi suorittaa pituusmuunnoksia. Siitä kuitenkin puuttuvat vielä kaunistelut, joita laitettiin pääohjelmaan ja joita kaavailtiin suunnitteluvaiheessa.
Kauneuskoulu¶
Aputulosteet on helppo lisätä
funktion
alkuun, koska ne menevät aikalailla samalla tavalla kuin pääohjelmassa
. def pituus():
print("Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Tuuma (in tai \")")
print("Jalka (ft tai ')")
print("Jaardi (yd)")
print("Maili (mi)")
print()
...
Suurempi haaste on tuottaa
merkkijonoja
, joiden keskellä esiintyy muuttujien
arvoja
. Puhehan oli, että tulosteet voisivat olla tämän näköisiä: 12.40" on 31.50 cm
Aiemmassa esimerkissä vilahti print-
funktiokutsu
, jolle oli annettu useampi kuin yksi argumentti
, ja se näytti tältä:print("Lisätty", nimi, "x", lkm)
Ja tuotti seuraavanlaisen merkkijonon, kun nimi- ja lkm-muuttujien arvot olivat "aasi" ja 1:
Lisätty aasi x 1
Print-funktiolle voidaan antaa ennaltamääräämätön määrä argumentteja, ja ne tulostetaan samalla riville välilyönnillä erotettuna. Kuten yllä olevasta esimerkistä näkyy, kukin argumentti voi olla erilainen: tässä osa on
literaaliarvoja
, osa muuttujia
– ja itse asiassa toisen muuttujan arvo on merkkijono
ja toisen kokonaisluku. Eli argumenteissa saa olla myös eri tietotyyppejä
sekaisin. Tästä voidaan ottaa mallia ensimmäiseen kokeiluun tuottaa muuttujia keskelle tulostettavaa riviä: print(arvo, "\"", "on", arvo * 2.54, "cm")
Tulkissa
kokeilemalla In [1]: arvo = 12.4
In [2]: print(arvo, "\"", "on", arvo * 2.54, "cm")
12.4 " on 31.496000000000002 cm
Lopputulos eroaa hieman halutusta: "-merkki ei ole kiinni numerossa, 12.4:stä puuttuu toinen desimaali (0 haluttiin näkyviin) ja lopputulosta ei ole pyöristetty. Kokeillaan ensin ratkaista selkeästi suurin ongelma, eli pyöristyksen puute, lisäämällä print-riville round-
funktiokutsu
2 desimaalin tarkkuuteen:In [1]: arvo = 12.4
In [2]: print(arvo, "\"", "on", round(arvo * 2.54, 2), "cm")
12.4 " on 31.5 cm
Nyt lopputuloksesta jää niin ikään puuttumaan lopusta 0. Lisäksi rivimme alkaa olla hyvin epämääräisen näköinen, ja siitä on vaikea sanoa, miltä lopputulos tulee näyttämään. Yleisesti ottaen on suotuisampaa käyttää erityisiä muotoilutyökaluja yhtään monimutkaisempien , joka löytyy
merkkijonojen
rakentamiseen. Pythonissa nämä työkalut kiteytyvät format
-metodiinmerkkijonometodien
joukosta. Se on käyttötarkoituksiltaan hyvin monipuolinen metodi ja vaatii jonkin verran uusien asioiden opettelua. Muotoillessa varsinainen merkkijono, jonka format-metodia kutsutaan, on eräänlainen sapluuna, johon on määritelty erikseen paikat sekä mahdolliset lisämääreet siihen sijoitettaville arvoille
. Samanhenkisesti siis kuin peruskoulun tehtävät, joissa lauseiden osia piti täydentää viivoille:_____" on _____ cm
Alaviivoja ei sentään käytetä, mutta vastaavaa merkintää kylläkin. Perusmalli muotoiltavasti merkkijonosta näyttää nimittäin tältä:
"{}\" on {} cm"
Koodissa aaltosulkumerkit osoittavat paikan, johon sijoitetaan jotain muuta. Se mitä muuta sinne sijoitetaan, annetaan format-
metodikutsulle
argumentteina
. Kuten print, myös format ottaa vastaan useita argumentteja, jotka voivat olla mitä tahansa arvoja. Oletustoimintana ensimmäinen argumentti sijoitetaan ensimmäisten aaltosulkujen paikalle, toinen toisten jne. Koska kyseessä on metodikutsu, format-kutsu tulee tämän sapluunamerkkijonon perään pisteellä erotettuna. Korvataan siis aiempi print-hirviö sellaisella, joka käyttää format-metodia:In [1]: arvo = 12.4
In [2]: print("{}\" on {} cm".format(arvo, round(arvo * 2.54, 2)))
12.4" on 31.5 cm
Jos tämä ei näytäkään kovin paljon selkeämmältä, yksi etu on ainakin se, että rivi on nyt paremmin jäsennelty:
merkkijonon
ulkoasu ilmenee kokonaisuudessaan vasemmalta, ja sinne sijoitettava data muotoiluineen taas oikealta, format-metodikutsun sisältä. Huomattavaa on myös, että moniargumenttisen printin pakottama välilyönti kunkin argumentin väliin voitiin nyt välttää.Avaimet aalloissa¶
Aaltosulut määrittävät siis
merkkijonossa
paikkoja
, joihin sijoitetaan Jotain Muuta (tm). Niiden määrittelyvoima ei kuitenkaan jää siihen. Niillä voidaan myös tarkemmin määritellä, mitä sijoitetaan. Tyhjilleen jätetyt aaltosulut toimivat siis siten, että ne saavat yksitellen arvonsa format
-metodikutsun
argumenteista
siinä järjestyksessä, kuin ne on annettu. Tässä on ongelmana se, että useita muuttujia sisältävät format-metodikutsut alkavat mennä vaikeiksi hahmottaa. Lisäksi ne ovat äärimmäisen virheherkkiä: aina merkkijonoa muuttaessa pitää muistaa päivittää myös format-metodikutsun argumentit, jos vaikka sijoitusten järjestys on muuttunut, tai sijoitettavien lukumäärä on muuttunut. Parempi tapa on antaa kullekin paikanpitimelle oma nimityksensä, ja nimetä argumentit vastaavasti.
"{us_arvo}\" on {si_arvo} cm".format(us_arvo=arvo, si_arvo=round(arvo * 2.54, 2))
Tälle menettelylle on nimikin:
avainsana-argumentit
(keyword arguments). Siinä missä ns. normaalisti funktioita
kutsuttaessa
argumenttien
järjestys määrittää sen, mikä parametri
saa minäkin argumentin arvon
, avainsanoilla voidaan yllä näkyvällä tavalla sanoa suoraan, mihin parametriin mikäkin arvo halutaan sijoittaa. Esimerkiksi round-funktion parametrien nimet ovat number ja ndigits, joten periaatteessa roundia voisi kutsua myös näin (parametrien nimet löytyvät funktion dokumentaatiosta): In [1]: round(ndigits=1, number=4.451)
Out[1]: 4.5
Format-metodi on hieman erikoisempi, koska sillä ei ole lainkaan pakollisia argumentteja, mutta sen sijaan sillä on teoriassa äärettömästi
valinnaisia
. Se ottaa siis vastaan mitä tahansa avainsana-argumentteja, kuten yllä nähtiin. Pythonin kehittäjät eivät nimittäin ole määritelleet erikseen us_arvo- ja si_arvo-nimisiä parametrejä
format
-metodille. Poikkeuksellisesti
funktiokutsun
sisällä =-merkin ympärille ei tule välilyöntejä. Sama pätee def-rivillä funktion
määrittelyssä. Tämä on ainoa poikkeus välilyöntien käytössä operaattorien
ympärillä. Avainsanojen
käytön etu nimenomaan format-metodin kanssa on se, että nimien antaminen sijoituspaikoille
ja sijoitettaville asioille tekee niiden yhteydestä huomattavasti helpomman hahmottaa, minkä lisäksi se on vähemmän virhealtis muutoksia tehtäessä. Lisäksi se mahdollistaa saman arvon
käytön useaan kertaan sapluunassa
ilman, että sitä täytyy toistaa format-metodikutsun argumenteissa. Otetaan esimerkkinä rivi ohjelmasta, joka tallentaa musiikkitiedostoja tietokoneelle siten, että kullakin formaatilla (esim. mp3, ogg) on oma kansionsa, jossa kullakin artistilla on oma kansionsa, jossa kullakin albumilla on oma kansionsa, ja lopulta itse tiedoston nimi on formaattia "Wolves in the Throne Room - 01 - Queen of the Borrowed Light.ogg" eli artistin nimi, kappaleen numero, kappaleen nimi ja pisteen jälkeen tiedostoformaatin pääte. Muuttujamäärittelyt:
In [1]: tyyppi = "ogg"
In [2]: artisti = "Wolves in the Throne Room"
In [3]: nimi = "Queen of the Borrowed Light"
In [4]: nro = "01"
In [5]: albumi = "Diadem of 12 Stars"
Muotoilu perinteisesti:
In [6]: "{}/{}/{}/{} - {} - {}.{}".format(tyyppi, artisti, albumi, artisti, nro, nimi, tyyppi)
Out[6]: 'ogg/Wolves in the Throne Room/Diadem of 12 Stars/Wolves in the Throne Room - 01 - Queen of the Borrowed Light.ogg'
Vastaavasti avainsanoilla:
In [7]: "{tyyppi}/{artisti}/{albumi}/{artisti} - {nro} - {nimi}.{tyyppi}".format(tyyppi=tyyppi, artisti=artisti, albumi=albumi, nro=nro, nimi=nimi)
Out[7]: 'ogg/Wolves in the Throne Room/Diadem of 12 Stars/Wolves in the Throne Room - 01 - Queen of the Borrowed Light.ogg'
Jälkimmäinen rivi on toki pidempi. Kuitenkin nimeämisen sapluuna on siinä huomattavasti selkeämpi. Ensimmäisen rivin kohdalla täytyy lukea myös
format-metodin
argumentteja
, jotta ymmärtää, miltä lopputulos näyttää. Lisäksi jälkimmäisen rivin tapauksessa format-metodilla ei ole samaa argumenttia useaan kertaan kuten perinteisessä tavassa. Tässä esimerkissä formatin parametreilla
ja argumenteilla sattuu olemaan myös samat nimet. Tämä ei haittaa, koska kuten muistamme viime viikolta, parametrit ovat olemassa erillisellä tasolla kuin muuttujat, joiden arvot niihin sijoitetaan. Päivitetään oma koodimme käyttämään avainsana-argumentteja
:def pituus():
print("Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Tuuma (in tai \")")
print("Jalka (ft tai ')")
print("Jaardi (yd)")
print("Maili (mi)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "in" or yksikko == "\"":
print("{us_arvo}\" on {si_arvo} cm".format(us_arvo=arvo, si_arvo=round(arvo * 2.54, 2)))
elif yksikko == "ft" or yksikko == "'":
print("{us_arvo}' on {si_arvo} cm".format(us_arvo=arvo, si_arvo=round(arvo * 30.48)))
elif yksikko == "yd":
print("{us_arvo} yd on {si_arvo} m".format(us_arvo=arvo, si_arvo=round(arvo * 0.9144)))
elif yksikko == "mi":
print("{us_arvo} mi on {si_arvo} km".format(us_arvo=arvo, si_arvo=round(arvo * 1.609344)))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Aaltojen muotoilijat¶
Sen lisäksi, että aaltosulkujen sisällä voidaan tarkemmin ohjata, mitä arvoja sijoitetaan mihin kohtaan
merkkijonoa
, myös niiden ulkoasua voi määritellä. Tässä materiaalissa on tästä kaksi erillistä esimerkkiä, joista molemmilla on omat käyttönsä jo peruskurssilla. Toinen on etunollien lisääminen kokonaislukuihin ja toinen liukulukujen
desimaalien lukumäärän määrittäminen. Etunollien lisäämistä käytetään mm. päivämäärien ja kellonaikojen käsittelyssä. Tyypillinen tietokoneella käsiteltävä aikaleima onkin tämän näköinen: "2015-06-29 09:46:06"
. Etunollien lisääminen tapahtuu seuraavan näköisellä määrittelyllä: In [1]: "{tunnit:02}".format(tunnit=9)
Out[1]: '09'
Paikanpitimen
ulkoasun määrittelyosa erotetaan nimeämisosasta kaksoispisteellä. Kaksoispistettä seuraava 0 merkitsee, että halutaan lisätä etunollia, kun taas sitä seuraava 2 kertoo, että lopputuloksen pitäisi olla vähintään 2 merkkiä pitkä - eli etunollia lisätään, jos sijoitettava arvo on lyhyempi kuin 2 merkkiä.Etunollia yleisempi tapaus on kuitenkin desimaalien lukumäärän määrittäminen. Muotoilu tehdään hyvin samanlaisella syntaksilla:
In [1]: "{pituus:.2f}".format(pituus=4.451)
Out[1]: '4.45'
Jossa nyt siis .-merkki kertoo, että sitä seuraava numero määrittelee näytettävien desimaalien lukumäärän. Muotoilun lopussa oleva f puolestaan kertoo, että lukua tulee käsitellä liukulukuna. Sen pois jättäminen aiheuttaa
poikkeuksen
: ValueError: Precision not allowed in integer format specifier
Python yrittää siis käsitellä lukua kokonaislukuna, jos ei erikseen määritellä, että sitä tulee käyttää liukulukuna. Kokonaisluvuille ei puolestaan voida määrittää näyttötarkkuutta, koska niillä ei ole desimaaleja. Näyttötarkkuuden määrittely tarkoittaa, että luvun loppuun lisätään tarvittaessa nollia:
In [1]: "{pituus:.2f}".format(pituus=4)
Out[1]: '4.00'
Samalla katoaa tarve käyttää round-
funktiota
, koska pyöristys hoituu muotoilun ohessa. Näinpä siis saamme nätimmän näköisiä rivejä ohjelmaamme: print("{us_arvo:.2f}\" on {si_arvo:.2f} cm".format(us_arvo=arvo, si_arvo=arvo * 2.54))
Koko funktio näyttää siis tässä vaiheessa tältä:
def pituus():
print("Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Tuuma (in tai \")")
print("Jalka (ft tai ')")
print("Jaardi (yd)")
print("Maili (mi)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "in" or yksikko == "\"":
print("{us_arvo:.2f}\" on {si_arvo:.2f} cm".format(us_arvo=arvo, si_arvo=arvo * 2.54))
elif yksikko == "ft" or yksikko == "'":
print("{us_arvo:.2f}' on {si_arvo:.2f} cm".format(us_arvo=arvo, si_arvo=arvo * 30.48))
elif yksikko == "yd":
print("{us_arvo:.2f} yd on {si_arvo:.2f} m".format(us_arvo=arvo, si_arvo=arvo * 0.9144))
elif yksikko == "mi":
print("{us_arvo:.2f} mi on {si_arvo:.2f} km".format(us_arvo=arvo, si_arvo=arvo * 1.609344))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Esimerkkisuoritus näyttää, miten kaunis tulos saatiin aikaan:
Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi Mahdolliset toiminnot: (P)ituus (M)assa (T)ilavuus (L)ämpotila Tee valintasi: p Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne Tuuma (in tai ") Jalka (ft tai ') Jaardi (yd) Maili (mi) Anna muutettava arvo: 65 Anna muutettava yksikkö: mi 65.00 mi on 104.61 km
Muotoilulla
format-metodin
avulla on todella paljon mahdollisuuksia. Niiden runsautta voi tutkia |Pythonin dokumentaatiosta. Langat yhteen¶
Hyviä uutisia: kaksi kolmesta puuttuvasta
funktiosta
ovat täysin identtisiä pituus-funktion kanssa. Ainoastaan yksiköt ja laskukaavat muuttuvat funktiosta toiseen. Niinpä voimme ilman kummempia jaaritteluja kirjoittaa nämä funktiot. Massan tapauksessa yksiköitä oli vain kaksi: unssi (oz) ja pauna (lb). print("Valitse painoyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Unssi (oz)")
print("Pauna (lb)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "oz":
print("{us_arvo:.2f} oz on {si_arvo:.2f} g".format(us_arvo=arvo, si_arvo=arvo * 28.349523125))
elif yksikko == "lb":
print("{us_arvo:.2f} lb on {si_arvo:.2f} kg".format(us_arvo=arvo, si_arvo=arvo * 0.45359237))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Ja tilavuuden tapauksessa:
print("Valitse nestetilavuusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Kupillinen (cp)")
print("Pintti (pt)")
print("Varttigallona (qt)")
print("Gallona (gal)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "cp":
print("{us_arvo:.2f} cp on {si_arvo:.2f} dl".format(us_arvo=arvo, si_arvo=arvo * 2.365882365))
elif yksikko == "pt":
print("{us_arvo:.2f} pt on {si_arvo:.2f} dl".format(us_arvo=arvo, si_arvo=arvo * 4.73176473))
elif yksikko == "qt":
print("{us_arvo:.2f} qt on {si_arvo:.2f} l".format(us_arvo=arvo, si_arvo=arvo * 0.946352946))
elif yksikko == "gal":
print("{us_arvo:.2f} gal on {si_arvo:.2f} l".format(us_arvo=arvo, si_arvo=arvo * 3.785411784))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
Lämpötilan tapauksessa on vain yksi yksikkö, jota muuntaa, joten yksikköä ei tarvitse kysyä erikseen. Funktiosta tuleekin siis huomattavasti lyhyempi. Fahrenheitin muunnoskaava Celsius-asteiksi on ihan omanlaistaan taidetta eikä suju suoraan kertolaskulla kuten muut muunnokset. Koska kaava on monimutkainen, on katsottu selkeämmäksi laskea se omaan muuttujaansa, ettei sitä tarvitse sisällyttää format-
metodikutsun
sulkeiden sisään. Funktio näyttäisi siis tältä: def lampotila():
print("Lämpötilamuunnos Fahrenheit-asteista Celsius-asteiksi")
fahrenheit = float(input("Anna lämpötila: "))
celsius = (5 / 9) * (fahrenheit - 32)
print("{us_arvo:.2f} °F on {si_arvo:.2f} °C".format(us_arvo=fahrenheit, si_arvo=celsius))
Määrittämämme neljän funktion (pituus, massa, tilavuus, lampotila) sisällä olevat koodit olisi voinut kirjoittaa myös sellaisenaan
pääohjelmaan
itse funktiokutsun
paikalle. Ohjelma toimisi täsmälleen samalla tavalla. Ehtorakenteessa
olisi vain kaksi sisennystasoa. Tavallaan siinä on nytkin, mutta tämä tosiasia on piilotettu osittamalla ohjelma funktioihin. Jos funktiorakenne purettaisiin ja kaikki olisi pääohjelmassa, se olisi tämän näköinen:print("Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi")
print("Mahdolliset toiminnot:")
print("(P)ituus")
print("(M)assa")
print("(T)ilavuus")
print("(L)ämpotila")
print()
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "p" or valinta == "pituus":
print("Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Tuuma (in tai \")")
print("Jalka (ft tai ')")
print("Jaardi (yd)")
print("Maili (mi)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "in" or yksikko == "\"":
print("{us_arvo:.2f}\" on {si_arvo:.2f} cm".format(us_arvo=arvo, si_arvo=arvo * 2.54))
elif yksikko == "ft" or yksikko == "'":
print("{us_arvo:.2f}' on {si_arvo:.2f} cm".format(us_arvo=arvo, si_arvo=arvo * 30.48))
elif yksikko == "yd":
print("{us_arvo:.2f} yd on {si_arvo:.2f} m".format(us_arvo=arvo, si_arvo=arvo * 0.9144))
elif yksikko == "mi":
print("{us_arvo:.2f} mi on {si_arvo:.2f} km".format(us_arvo=arvo, si_arvo=arvo * 1.609344))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
elif valinta == "m" or valinta == "massa":
print("Valitse painoyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Unssi (oz)")
print("Pauna (lb)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "oz":
print("{us_arvo:.2f} oz on {si_arvo:.2f} g".format(us_arvo=arvo, si_arvo=arvo * 28.349523125))
elif yksikko == "lb":
print("{us_arvo:.2f} lb on {si_arvo:.2f} kg".format(us_arvo=arvo, si_arvo=arvo * 0.45359237))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
elif valinta == "t" or valinta == "tilavuus":
print("Valitse nestetilavuusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Kupillinen (cp)")
print("Pintti (pt)")
print("Varttigallona (qt)")
print("Gallona (gal)")
print()
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "cp":
print("{us_arvo:.2f} cp on {si_arvo:.2f} dl".format(us_arvo=arvo, si_arvo=arvo * 2.365882365))
elif yksikko == "pt":
print("{us_arvo:.2f} pt on {si_arvo:.2f} dl".format(us_arvo=arvo, si_arvo=arvo * 4.73176473))
elif yksikko == "qt":
print("{us_arvo:.2f} qt on {si_arvo:.2f} l".format(us_arvo=arvo, si_arvo=arvo * 0.946352946))
elif yksikko == "gal":
print("{us_arvo:.2f} gal on {si_arvo:.2f} l".format(us_arvo=arvo, si_arvo=arvo * 3.785411784))
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
elif valinta == "l" or valinta == "lämpötila":
print("Lämpötilamuunnos Fahrenheit-asteista Celsius-asteiksi")
fahrenheit = float(input("Anna lämpötila: "))
celsius = (5 / 9) * (fahrenheit - 32)
print("{us_arvo:.2f} °F on {si_arvo:.2f} °C".format(us_arvo=fahrenheit, si_arvo=celsius))
else:
print("Valitsemaasi toimintoa ei ole olemassa")
Tästä hirveästä litaniasta on huomattavasti hankalampaa hahmottaa ohjelman toiminta. Esimerkiksi tuo uloin
ehtorakenne
muuttuu niin pitkäksi, että rakenteen aloittava if ja lopettava else eivät mahdu samalle ruudulle. Jos tätä vertaa vielä funktioita käyttävään pääohjelmaan
, pitäisi olla aika selkeää mitä etua on jo selkeyden kannalta siitä, että ohjelma jaetaan hallittaviin osiin.print("Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi")
print("Mahdolliset toiminnot:")
print("(P)ituus")
print("(M)assa")
print("(T)ilavuus")
print("(L)ämpotila")
print()
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "p" or valinta == "pituus":
pituus()
elif valinta == "m" or valinta == "massa":
massa()
elif valinta == "t" or valinta == "tilavuus":
tilavuus()
elif valinta == "l" or valinta == "lämpötila":
lampotila()
else:
print("Valitsemaasi toimintoa ei ole olemassa")
Alla ohjelma joka ollaan tähän asti saatu aikaan
Sana kirjoista¶
Jos tarkastellaan yksikkömuuntimen ehtorakenteita, niissä havaitaan kohtalaisen selkeä kaava: ehtorakenteella valitaan pääasiassa pelkkä kerroin. Mitä jos kerroin voitaisiin valita jollain toisella tavalla? Tällöin nykyinen ehtorakenteeksi tehty muunnos voitaisiin kirjoittaa pelkkänä kaavana
arvo * kerroin
. Mutta mistä kerroin sitten saadaan? Voitaisiin tietenkin valita kerroin ehtorakenteessa ennen kaavaa, tyyliin: def pituus():
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
if yksikko == "in" or yksikko == "\"":
kerroin = 2.54 / 100
elif yksikko == "ft" or yksikko == "'":
kerroin = 30.48 / 100
elif yksikko == "yd":
kerroin = 0.9144
elif yksikko == "mi":
kerroin = 1.609344 * 1000
else:
print("Valitsemaasi yksikköä ei voida muuntaa")
return
print("{us_arvo:.3f} {yksikko} on {si_arvo:.3f} m".format(yksikko=yksikko, us_arvo=arvo, si_arvo=arvo * kerroin))
Tässä ei tosin ole kauheasti eroa, paitsi nyt kaikki muutetaan metreiksi joka täytyy huomioida kertoimissa. Asiaa voi kuitenkin parantaa ottamalla käyttöön uuden hyödyllisen tietotyypin.
Osaamistavoitteet¶
Tämän osion jälkeen tiedät mitä ovat Pythonin sanakirjat ja pari erilaista skenaariota joissa niillä saadaan tehtyä nätin näköistä ja dynaamista koodia (ts. koodia, jonka toimintaa on helppo säätää ilman suuria muutoksia). Lisäksi pureudumme muuntuvien ja muuntumattomien arvojen mysteereihin, joiden kautta viimein selviää miksi on ollut tärkeää teroittaa, että muuttuja on pelkästään viittaus arvoon.
Kaaoksen sanakirja¶
Sanakirja on
tietorakenne
. Tietorakenteet yleisesti ovat tietotyyppejä, jotka sisältävät muita arvoja
. Yleensä on myös suotavaa, että arvot liittyvät toisiinsa jollain tapaa. Tietorakenteilla on myös oma sisäänrakennettu tapa jolla niihin sijoitetaan arvoja sekä haetaan siellä olevia arvoja. Nimelleen omaisesti sanakirjassa tämä tapahtuu hakusanojen avulla. Näitä hakusanoja kutsutaan avaimiksi
ja ne voivat olla mitä tahansa muuntumattomia tietotyyppejä - kuitenkin melkein aina ne ovat merkkijonoja
. Sanakirjalle on myös ominaista, että siellä olevat arvot eivät ole missään tietyssä järjestyksessä - ainakaan ennen Python 3.7-versiota, josta eteenpäin arvot ovat siinä järjestyksessä kuin ne on lisätty.Sanakirjassa - kuten oikeassakin sanakirjassa - jokainen hakusana eli avain on sidottu johonkin arvoon. Tätä yhteyttä kutsutaan avain-arvo-pariksi. Siinä missä avain on tyypillisesti merkkijono, arvot voivat olla mitä tahansa - myös
tietorakenteita
!. Vastaavasti sama avain ei voi esiintyä sanakirjassa kuin kerran, mutta arvojen puolella tätä rajoitusta ei ole. Hakeminen on yksisuuntaista, eli ainoastaan avaimella voi löytää arvon - ei toisinpäin. Sanakirja merkitään aaltosulkeilla. Kaikki aaltosulkeiden välissä oleva tulkitaan
sanakirjan
määrittelyksi. Määrittelyn syntaksi
vaatii, että avain-arvo-parit erotetaan toisistaan pilkuilla, ja avain
erotetaan arvosta
kaksoispisteellä. Koska sanakirjan määrittely sisältää tyypillisesti paljon merkkejä, se usein jaetaan monelle riville - määrittely voidaan katkoa sekä aaltosulkeiden että pilkkujen kohdalta rivinvaihdoilla. Alla oleva esimerkki saattaa vinkata mihin suuntaan tämä ajatus sanakirjojen käytöstä on kehittymässä: pituuskertoimet = {
"in": 0.0254,
"ft": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
Esimerkistä nähdään myös, että aaltosulkeiden välissä oleva määrittely on selkeyden nimissä hyvä
sisentää
yhden tason syvemmälle. Aeimmasta poiketen tällä sisennyksellä ei ole koodin suorituksen kannalta mitään vaikutusta, mutta selkeyden kannalta paljonkin - nyt nähdään helposti missä kohdin sanakirjan määrittely loppuu ilman, että tarvii etsiä missä aaltosulut päättyvät. Avaimet käteen¶
Äsken mainostettiin, että
sanakirjasta
saa arvon
avainta
vastaan. Ihan hyvä, mutta miten? Tähän on kaksi tapaa: tietorakenteisiin
liittyvä yleinen hakusyntaksi sekä sanakirjojen oma get-metodi
. Hakusyntaksi toimii siten, että tietorakenteen perään laitetaan hakasulut, joiden sisällä annetaan hakuarvo - tässä tapauksessa siis avain. Eli jos vaikka aiemmin määritetystä sanakirjasta halutaan jaardin kerroin ulos:In [1]: pituuskertoimet = {
...: "in": 0.0254,
...: "ft": 0.3048,
...: "yd": 0.9144,
...: "mi": 1609.344
...: }
...:
In [2]: kerroin = pituuskertoimet["yd"]
In [3]: kerroin
Out[3]: 0.9144
Haku sanakirjasta
palauttaa
avainta vastaavan arvon, joten se voidaan napata kiinni muuttujaan. Se voidaan myös hakea suoraan laskukaavaan, joten miten olisi hetki sitten esitellyn ehtorakenteen
korvaaminen sanakirjalla
ja avainhaulla...? Avainkin voidaan nimittäin hakea muuttujasta:In [4]: yksikko = "ft"
In [5]: print(pituuskertoimet[yksikko])
0.3048
Ja näitä tietoja soveltava uutuudenkiiltävä pituus-funktio:
def pituus():
pituuskertoimet = {
"in": 0.0254,
"ft": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
print("{us_arvo:.3f} {yksikko} on {si_arvo:.3f} m".format(
yksikko=yksikko,
us_arvo=arvo,
si_arvo=arvo * pituuskertoimet[yksikko]
))
Tässä on myös harrastettu pitkäksi kasvaneen
metodikutsurivin
jakamista osiin. Samalla tavalla kuin sanakirjojen määrittelyssä, myös metodi- ja funktiokutsun voi pätkiä rivinvaihdoilla sulkujen ja pilkkujen kohdalla. Jälleen sisennystä on käytetty selkeyttä varten. Itse koodin toimintaa on paras avata animaatiolla.Tässä koodissa on tosin kaksi (uutta) puutetta: tuumaa ja jalkaa ei voi hakea symbolilla; ja mitä tapahtuu jos käyttäjä valitsee yksikön jota ei tueta?. Ensimmäinen on helppo ratkaista. Kuten todettua, sama arvo voi löytyä useammalla avaimella, joten ne voidaan vain lisätä sanakirjaan:
pituuskertoimet = {
"in": 0.0254,
"\"": 0.0254,
"ft": 0.3048,
"'": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
Tässä näkyy samalla aiemmin mainostettu dynaamisuus:
sanakirjaa muokkaamalla siinä missä ehtolauseratkaisussa uuden yksikön lisääminen vaatisi
funktion
toiminta muuttuu pelkästään tätä sanakirjaa muokkaamalla siinä missä ehtolauseratkaisussa uuden yksikön lisääminen vaatisi
ehtorakenteen
muokkaamista.Kun tiedetään, että olemattoman avaimen käytöstä seuraa
poikkeus
, luonteva askel on tietenkin käsitellä se. Jos siis käyttäjän antama syöte
aiheuttaa poikkeuksen, tulostetaan virheviesti. Tällä hetkellä try:n sisään tulee laittaa tuo print-hirviölauseke kokonaisuudessaan. Vaikka lause onkin pitkä, se ei itse asiassa ole kauhean räjähdysherkkä. Riskiä ottaa vahingossa kiinni jokin muu poikkeus kuin se jota ajateltiin ei siis ole. def pituus():
pituuskertoimet = {
"in": 0.0254,
"\"": 0.0254,
"ft": 0.3048,
"'": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
try:
print("{us_arvo:.3f} {yksikko} on {si_arvo:.3f} m".format(
yksikko=yksikko,
us_arvo=arvo,
si_arvo=arvo * pituuskertoimet[yksikko]
))
except EdellisenTehtävänVastaus:
print("Valitsemaasi yksikköä ei voida muuntaa")
Virheellisen arvon syöttäminen jätetään edelleen käsittelemättä - se tehdään kunnolla seuraavassa materiaalissa. Oikeastaan
sanakirjan
määrittely funktion sisällä ei myöskään ole järkevää, koska sitä käytetään pelkästään arvojen
lukemiseen. Tällä hetkellä se luodaan uudestaan aina kun funktiota
kutsutaan. Se kanattaakin siis siirtää vakioksi
ohjelman alkuun (mahdollisten import-lauseiden alle). Eli näin, tuomatta vielä muuta ohjelmaa mukaan kuvioon. PITUUSKERTOIMET = {
"in": 0.0254,
"\"": 0.0254,
"ft": 0.3048,
"'": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
def pituus():
arvo = float(input("Anna muutettava arvo: "))
yksikko = input("Anna muutettava yksikkö: ")
try:
print("{us_arvo:.3f} {yksikko} on {si_arvo:.3f} m".format(
yksikko=yksikko,
us_arvo=arvo,
si_arvo=arvo * PITUUSKERTOIMET[yksikko]
))
except EdellisenTehtävänVastaus:
print("Valitsemaasi yksikköä ei voida muuntaa")
Saman voi tehdä muillekin funktioille. Siirrytään kuitenkin asiassa eteenpäin sanakirjojen muokkaamiseen.
Sanakirjat vasaran alla¶
Tietorakenteille
on usein myös ominaista se, että niitä voidaan muokata. Sanakirjaan
voidaankin ohjelman suorituksen aikana sekä lisätä uusia avain
-arvo
-pareja. Avaimiin liitettyjä arvoja voi myös vaihtaa toisiin. Tehdään tätä tarkoitusta varten uusi funktio. Kuvitellaan ohjelma, joka käsittelee mittaustuloksia erinäisistä lähteistä. Yksi tulos on määritelty sanakirjana, jossa on kaksi avain-arvo-paria: mittayksikkö ja numeerinen arvo, esim:mittaus = {
"yksikko": "in",
"arvo": 4.64
}
Jatkokäsittelyn kannalta olisi parempi, että kaikki mittaukset olisivat samaa yksikköä. Äsken kasailtua pituus-
funktiota
ja sitä varten luotua kertoimet-sanakirjaa voidaan hieman muokkaamalla käyttää tähän tarkoitukseen. Tarkoitus on siis, että uusi funktio saa käsiteltäväksi yhden mittauksen, jonka se muuttaa metreiksi. Funktion määrittelyriviin tulee nyt siis parametri. Samalla arvon ja yksikön kysyminen
poistuu ohjelmasta, ja ne otetaan suoraan mittaus-sanakirjasta edellisessä osiossa opitulla avainhaulla. Myös arvo metreinä voidaan laskea suoraan muuttujaan.def muunna_metreiksi(mittaus):
arvo = mittaus["arvo"]
yksikko = mittaus["yksikko"]
metrit = arvo * PITUUSKERTOIMET[yksikko]
Kysymykseksi jääkin enää: miten uudet arvot saadaan laitettua sanakirjaan. Menetelmä on itse asiassa hyvin pitkälti sama kuin avainhaussa. Tiedetään, että
muuttujan
rooli vaihtuu, jos se on sijoitusoperaattorin
vasemmalle puolella: siihen sijoitetaan sen sijaan, että haettaisiin sen arvo. Avainhaun syntaksi
toimii samalla tavalla: jos se on sijoitusoperaattorin vasemmalle puolella, avaimeen liitetty arvo korvataan uudella:In [1]: mittaus = {
...: "yksikko": "in",
...: "arvo": 4.64
...: }
In [2]: mittaus["yksikko"] = "m"
In [3]: mittaus
Out[3]: {'yksikko': 'm', 'arvo': 4.64}
Toiminta näyttää yksinkertaiselta, ja onkin sitä, mutta tähän liittyy yksi täysin uusi asia:
sanakirja
on muuntuva
. Huomaa, että missään vaiheessa esimerkkiä ei luoda uutta muuttujaa
- sanakirjan muokkaus kohdistuu suoraan alkuperäiseen sanakirjaan. Aiemmin tässä materiaalissa teroitettiin, että merkkijono
on muuntumaton
tietotyyppi ja kaikki metodit
jotka "muuttavat" merkkijonoa tosiasiassa tekevät siitä muutetun kopion. Periaatteessa mittausyksikön muokkaus voitaisiin tehdä nimittän myös merkkijonoilla ja replacella:In [1]: mittaus = "12.984 in"
In [2]: mittaus.replace("in", "m")
Out[2]: '12.984 m'
In [3]: mittaus
Out[3]: '12.984 in'
In [4]: mittaus = mittaus.replace("in", "m")
In [5]: mittaus
Out[5]: '12.984 m'
Esimerkissä ensin yritetään "muokata" mittausta, mutta mittaus-muuttujan sisältöä katsottaessa huomataan, että replacen vaikutus ei jäänyt voimaan. Vasta neljäs rivi saa aikaan muutoksen, mutta kyseessä ei ole enää sama mittaus-muuttuja vaan uusi jolla sattuu olemaan sama nimi. Sanakirjat - kuten muutkin muuntuvat tietotyypit - toimiva juuri päinvastoin: pääasiassa niitä muokkaavat operaatiot sörkkivät suoraan alkuperäistä. Sanakirjojen kanssa onkin hyvä pitää mielessä, että se itsessään on vain kokoelma
viittauksia
arvoihin
. Arvot itsessään eivät siis ole varsinaisesti sanakirjan sisällä, vaan ovat olemassa muistissa ihan samalla tavalla kuin muutkin arvot. Se, että ne ovat sanakirjan avaimia
vastaavia arvoja ei siis tee niistä millään tapaa erikoisia. Jos jossain tapauksessa oikeasti haluat kaksi samanlaista
sanakirjaa
, tämä onnistuu sanakirjan copy-metodilla
. Se palauttaa nimensä mukaisesti kopion sanakirjasta. Me haluamme kuitenkin tehdä funktion, joka nimenomaan muokkaa alkuperäistä mittausta. Niinpä siis muokkaus tapahtuu näin:def muunna_metreiksi(mittaus):
arvo = mittaus["arvo"]
yksikko = mittaus["yksikko"]
metrit = arvo * PITUUSKERTOIMET[yksikko]
mittaus["yksikko"] = "m"
mittaus["arvo"] = metrit
Puuttuuko tästä jotain (muuta kuin poikkeusten käsittely)?
Funktiossa
ei ole lainkaan return-lausetta, mikä vaikuttaa kovasti epäilyttävältä... Funktio on kuitenkin ihan oikein. Koska muuntuvan
tietorakenteen muokkaus vaikuttaa suoraan muistissa olevaan dataan, muutos heijastuu kaikkialle missä tätä sanakirjaa käsitellään. Osoitetaan tämä lisäämällä yksinkertainen pääohjelma. Huomaa miten tulostus tehdään kahdesti täsmälleen samanlaisella rivillä, ja kuinka muunna_metreiksi-funktiokutsun
paluuarvoa
ei oteta ollenkaan talteen. PITUUSKERTOIMET = {
"in": 0.0254,
"\"": 0.0254,
"ft": 0.3048,
"'": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
def muunna_metreiksi(mittaus):
arvo = mittaus["arvo"]
yksikko = mittaus["yksikko"]
metrit = arvo * PITUUSKERTOIMET[yksikko]
mittaus["yksikko"] = "m"
mittaus["arvo"] = metrit
mittaus = {"yksikko": "in", "arvo": 4.64}
print("{:.3f} {}".format(mittaus["arvo"], mittaus["yksikko"]))
print("on")
muunna_metreiksi(mittaus)
print("{:.3f} {}".format(mittaus["arvo"], mittaus["yksikko"]))
Suoritetaan ja nähdään että:
4.64 in on 0.118 m
Entä se
poikkeusten
käsittely. Edelleen on mahdollista, että funktiolle tulee sanakirja
, jossa on pituusyksikkö, jota ei osata muuntaa (edelleen oletetaan, että arvot ovat aina liukulukuja). Tehdään käsittely siten, että opitaan samalla jotain uutta. Jos mittausta ei voida muuntaa, lisätään siihen uusi avain-arvo-pari: avain "virheellinen", joka on totuusarvo
ja tarkemmin True, jos mittausta ei voitu muuntaa. Avaimen lisääminen tapahtuu samalla tavalla kuin olemassaolevan avaimen arvon muuttaminen - eli tämäkin toimii kuten muuttujien
kanssa. Poikkeuksen löytäminen tapahtuu laittamalla avainhaku try-lohkoon
, virhetapaus except-haaraan ja sanakirjan päivitys else-haaraan: def muunna_metreiksi(mittaus):
arvo = mittaus["arvo"]
yksikko = mittaus["yksikko"]
try:
metrit = arvo * PITUUSKERTOIMET[yksikko]
except KeyError:
mittaus["virheellinen"] = True
else:
mittaus["yksikko"] = "m"
mittaus["arvo"] = metrit
Se uusi juttu, joka opitaan, on itse asiassa se, että on olemassa toinenkin tapa hakea sanakirjasta
arvoja
avaimella
: get-metodi
. Metodin käyttö eroaa avainhausta siten, että se ei aiheuta poikkeusta
, jos avainta ei löydy. Poikkeuksen sijaan get-metodi palauttaa
ennaltamäärätyn oletusarvon, joka on None, jos toisin ei ole osoitettu. In [1]: mittaus_1 = {"yksikko": "m", "arvo": 1.0}
In [2]: mittaus_2 = {"yksikko": "aasi", "arvo": 3.63, "virheellinen": True}
In [3]: mittaus_1.get("virheellinen", False)
Out[3]: False
In [4]: mittaus_2.get("virheellinen", False)
Out[4]: True
Virheellisen mittauksen käsittely voidaan nyt lisätä pääohjelmaan demonstraatiomielessä:
PITUUSKERTOIMET = {
"in": 0.0254,
"\"": 0.0254,
"ft": 0.3048,
"'": 0.3048,
"yd": 0.9144,
"mi": 1609.344
}
def muunna_metreiksi(mittaus):
arvo = mittaus["arvo"]
yksikko = mittaus["yksikko"]
try:
metrit = arvo * PITUUSKERTOIMET[yksikko]
except KeyError:
mittaus["virheellinen"] = True
else:
mittaus["yksikko"] = "m"
mittaus["arvo"] = metrit
mittaus = {"yksikko": "aasi", "arvo": 4.64}
print("{:.3f} {}".format(mittaus["arvo"], mittaus["yksikko"]))
print("on")
muunna_metreiksi(mittaus)
if not mittaus.get("virheellinen", False):
print("{:.3f} {}".format(mittaus["arvo"], mittaus["yksikko"]))
else:
print("Virheellinen yksikkö")
Jolloin
4.64 aasi on Virheellinen yksikkö
Tämä pääohjelma on aika teennäinen esimerkki. Oikea käyttökonteksti tehdylle muunnosfunktiolle vaatii kuitenkin seuraavan materiaalin opiskelua. Tässä on kuitenkin opittu perusteet
sanakirjoista
sekä siitä mitä muuntuva arvo
tarkoittaa. Loppusanat¶
Tässä materiaalissa olemme omaksuneet kolme keskeistä ohjelmointitaitoa: tekstin käsittely ja tuottaminen
merkkijonoilla
; poikkeustilanteiden
käsittely try-except-rakenteilla; ja keskeisimpänä ohjelman haarautumisen hallinta ehtorakenteilla
. Ohjelmamme muuttuivat yksinkertaisesta yhden operaation tekevistä laskurutiineista sellaisiksi, joissa käyttäjä voi oikeasti tehdä valintoja ja saada sen perusteella erilaisia lopputuloksia. Havaitsimme myös, että ohjelmoinnissa yksi suuri osa työstä on se, että ns. insinööriratkaisu muutetaan sellaiseksi, että sitä uskaltaa näyttää kenelle tahansa. Toimiva logiikka onkin vain yksi osa ohjelmaa – myös käyttökokemus on tärkeä. Materialissa tutustuttiin myös
tietorakenteisiin
ja niiden joukosta tarkemmin sanakirjoihin
. Koska sanakirja on materiaalissa esiintyvistä tietotyypeistä
ensimmäinen muuntuva
, käsittelimme myös sitä miten muuntuvat tietotyypit eroavat ratkaisevasti muuntumattomista
, kuten esimerkiksi merkkijonoista
. Huomioitavaa tässä materiaalissa on, että jätimme jälkimmäisessä esimerkissä käyttäjän
syötteet
tarkistamatta, jotta koodilistaukset pysyisivät paremmin luettavina. Kaikkialla, missä käyttäjältä pyydetään lukuja, pitäisi tietenkin olla try-except-rakenne, jotta ohjelma käyttäytyy nätisti. Tutustumme ensi viikolla siihen, miten tämä voidaan hoitaa ilman, että koodiin tulee lisää lukemista vaikeuttavaa sisältöä. Toinen ilmeinen puute yksikkömuunninohjelmassa on se, että yhden muunnoksen jälkeen ohjelma pitää käynnistää uudestaan. Myös tähän epäkohtaan pääsemme jatkossa tarttumaan.Kuvalähteet¶
- alkuperäinen lisenssi: CC-BY 2.0
- alkuperäinen lisenssi: CC-BY-NC 2.0 (teksti lisätty)
- alkuperäinen lisenssi: CC-BY 2.0 (teksti lisätty)
- alkuperäinen lisenssi: CC-BY 2.0 (teksti lisätty)
- alkuperäinen lisenssi: CC-BY 2.0
- alkuperäinen lisenssi: CC-BY 2.0 (kuvaa rajattu)
- alkuperäinen lisenssi: CC-BY 2.0 (teksti lisätty)
Anna palautetta
Kommentteja materiaalista?