3. Materiaali: Kehää kiertävät listat¶
Kaikki tämä on toistoa¶
Ohjelmointitaitojen evoluutio tällä kurssilla on tahdiltaan ripeä. Ensimmäisen materiaalin suoraviivaisista laskutoimituksista päästiin viime viikolla jo haarautuviin ohjelmiin, jotka kykenivät kyselemään käyttäjältä tietoja ja toimimaan niiden perusteella. Ohjelmamme ovat toistaiseksi käsitelleet dataa ainoastaan yksittäisinä arvoina, joiden lukumäärä on aina ollut ennalta määrätty. Olemme esimerkiksi käyttäneet muuttujia
luku_1
ja luku_2
käsitellessämme tasan kahta numeroarvoa.Tosielämässä on kuitenkin helppo keksiä skenaarioita, joissa käsitellään ennalta tuntematonta määrää dataa. Vaikka määrä olisi tunnettukin, suuret datamäärät aiheuttavat silti ongelmia: jos meillä on tuhat numeroarvoa, tarvitaan nykyisellä osaamisella myös tuhat koodiriviä näiden kaikkien käsittelyyn.
Tämän materiaalin aiheina ovat kunnianhimoisesti sekä listat että toistorakenteet. Ne ovat ohjelmointipalapelimme viimeiset palaset. Niitä ei tosin käydä aivan täysin läpi, mutta saaduilla työkaluilla pystyy kuitenkin teoriassa ratkomaan melkein mitä tahansa ongelmia. Käytännössä toki tosimaailman ohjelmointiin kuuluu paljon muutakin kuin tässä ja edellisissä materiaaleissa esitellyt peruskäsitteet. Kaikki asiat kuitenkin pohjimmiltaan perustuvat näille peruskäsitteille, ja useimmiten myöhemmin opeteltavissa asioissa on pikemminkin kyse siitä, ettei tarvitse tehdä joka asiaa itse aloittamalla aina peruspalikoista.
Merkittävimmäksi rajoitteeksemme tämän luvun jälkeen jää se, ettei meillä ole vielä pääsyä tietokoneella oleviin tiedostoihin, joten esimerkiksi mittausdatan käsittely ei ole ulottuvillamme. Meiltä myös puuttuu vielä kyky muokata listoja, joten mm. miinojen sijoittelu listoilla kuvattuun kenttään ei ihan vielä tule onnistumaan.
Kyselytunti 2: Kysymykset ovat ikuisia¶
Viime materiaalin yhtenä lopputuloksena syntyi ohjelma, joka suoritti yksikkömuunnoksia imperiaalisista yksiköistä SI-järjestelmään. Ohjelmassa oli yksi yleisesti toistuva kehoite:
Anna muutettava arvo:
. Jätimme viime kerralla toteuttamatta arvon tarkistuksen esimerkin selkeyden nimissä, mutta tietenkin, jos kyseessä olisi ollut oikeaan käyttöön suunnattu ohjelma, sen olisi pitänyt ehdottomasti selvitä myös virhetilanteista kaatumatta. Toinen ilmeinen epäkohta ohjelmassa oli, että se piti aina käynnistää uudestaan yhden yksikkömuunnoksen jälkeen.Oppimistavoitteet: Tässä osiossa opitaan luomaan yhdenlainen silmukka ja toteuttamaan sitä käyttämällä ohjelma, joka toistaa tiettyjä koodinpätkiä itsestään ennaltamääräämättömän määrän kertoja. Käymme läpi tämän yleisen ratkaisun filosofiaa ja esitämme tietysti, miten sellainen toteutetaan Pythonissa konkreettisesti. Tämän osuuden jälkeen osaat mm. tehdä fiksuja kyselyfunktioita, jotka pyytävät syötettä käyttäjältä itsepintaisesti niin kauan, että saatu syöte on oikeanlainen.
Koodille ehdollista pakkotoistoa¶
Toistorakenteet
ovat ehtorakenteiden
sukulaisia, ja näillä kahdella onkin yhteinen nimi: ohjausrakenteet
. Nimi viittaa siihen, että ne ovat rakenteita, jotka ohjaavat tavalla tai toisella ohjelman suoritusta. Siinä missä ehtorakenteet luovat haaroja, toistorakenteet luovat toistoa. Niitä kutsutaan tästä syystä myös silmukoiksi.Silmukoita on Pythonissa (ja useissa muissakin kielissä) kahta lajia. Näistä ensimmäinen on erityisen läheistä sukua ehtorakenteille. Siinä missä
ehtolause
määrittää ehdon
, jolla sen alaisuudessa oleva koodi suoritetaan, vastaava toistorakenne määrittää ehdon, jonka ollessa voimassa sen alaisuudessa olevaa koodia suoritetaan toistuvasti – niin kauan siis kuin ehto on tosi. Ehdon totuudellisuus tarkistetaan uudestaan jokaisen kierroksen alussa. Koodiesimerkkinä:syote = ""
while len(syote) < 8:
syote = input("Kirjoita vähintään 8 merkkiä pitkä sana: ")
print("Kirjoitit sanan", syote)
Tässä käytetään ehdossa
len
-funktiota
, joka palauttaa argumentiksi
annetun merkkijonon
(tai muun sekvenssin
) pituuden. Koodi siis kysyy käyttäjältä syötettä
, kunnes tämä antaa syötteen, jossa on vähintään 8 merkkiä. Huomaa, että ehto on tästä käänteinen: niin kauan kuin syote-merkkijonon pituus on pienempi kuin 8, suoritetaan koodiriviä, jolla käyttäjältä kysytään syötettä.Silmukoista
while
sopii juuri tämän kaltaisiin tapauksiin, joissa lopetus riippuu jostain silmukan sisällä tapahtuvasta ennakoimattomattomasta tapahtumasta. Tässä esimerkissä koodin kirjoittajan on täysin mahdotonta tietää, kuinka monta kertaa käyttäjä tulee syöttämään sanan, jossa on alle 8 merkkiä. Lopetuksen täytyy tapahtua juuri sillä toistolla, jolla käyttäjä syöttää oikeanlaisen merkkijonon. Niinpä while-silmukka
sopii tilanteisiin, joissa toistojen lukumäärää ei voida mitenkään päätellä ennen silmukan suorittamista.Haluamme rakentaa ohjelman, jossa käyttäjältä kysytään numeroarvoa niin kauan, että tämä oikeasti syöttää kelvollisen numeroarvon.
Toistorakenteisin
, ja nimenomaan ehdolliseen toistorakenteeseen, tässä määrittelyssä viittaa hyvinkin selkeästi ilmaisu "niin kauan, että". Ohjelmassa on selkeästi tarve toistaa jokin kohta koodissa, ja lisäksi toistojen lukumäärä ei ole tiedossa koodia kirjoittaessa, vaan se riippuu käyttäjästä. Tämän kääntämisessä koodiksi on tosin pienoinen mutka: syötteiden numerokelvollisuutta on aiemmin tarkasteltu try-rakenteilla eikä ehdoilla
. Teoriassa kelvollisen syötteen voi tarkistaa ilman try-rakenteen käyttöäkin, mutta sen sovittaminen ehtolauseen
ehdoksi on vaikeaa.Vankilapako¶
Selvitetään ongelman ratkaisemiseksi, millä kaikilla tavoilla
silmukan
suoritus voi päättyä. Toistaiseksi tunnemme vain yhden tavan: while-silmukan
suoritus päättyy, kun sen ehto
ei ole enää tosi. Silmukka voi kuitenkin päättyä muillakin tavoilla. Jos silmukassa tapahtuu poikkeus
, jota ei käsitellä silmukan sisällä try-rakenteella, silmukan suoritus päättyy:In [1]: while True:
...: luku = float(input("Anna luku: "))
...:
Anna luku: 15
Anna luku: aasi
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-bd285c9d1f25> in <module>()
1 while True:
----> 2 luku = float(input(": "))
3
Tämän esimerkin silmukan ehto on True, joten se ei selkeästikään voi päättyä siihen, että ehto muuttuisi epätodeksi. Silmukkaa suorittaessa kuitenkin tapahtuu poikkeus. Käyttäjän
syötettä
ei voida muuttaa liukuluvuksi, mikä päättää silmukan välittömästi. Jos sen sijaan silmukan sisällä olisi try-rakenne seuraavalla tavalla, silmukkaa ei voisi päättää virheellisellä syötteellä:In [1]: while True:
...: try:
...: luku = float(input("Anna luku: "))
...: except ValueError:
...: print("Et antanut lukua")
...:
Anna luku: 15
Anna luku: aasi
Et antanut lukua
Anna luku: 40
Anna luku:
Silmukan
lopettamiseen löytyy myös siistimpiä tapoja kuin yllä olevan tehtävän vastaus. Mikäli silmukka on funktiossa
, sen suoritus päättyy kun kohdataan funktion return-lause silmukan sisällä. Silmukan suoritus päättyy myös mikäli kohdataan break
-lause. Näistä jälkimmäinen on uusi temppu, mutta varsin yksinkertainen. Kun ohjelman suoritus kohtaa break-lauseen
, se hyppää välittömästi ulos sillä hetkellä meneillään olevasta silmukasta, ja jatkaa suoritusta silmukan jälkeisestä koodista. Lisätään break-lause ylhäällä olevaan try-rakenteeseen elsen avulla:In [1]: while True:
...: try:
...: luku = float(input("Anna luku: "))
...: except ValueError:
...: print("Et antanut lukua")
...: else:
...: break
...:
Anna luku: aasi
Et antanut lukua
Anna luku: 40
On muistettava, että try-rakenteen else-osaan mennään mikäli try-osan sisällä oleva koodi suorittuu kokonaan ilman ongelmia. Sijoitetaan tämä koodinpätkä aiemmin kasattuun koodiin. Esimerkkinä olkoon pituus-funktio:
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()
yksikko = input("Anna muutettava yksikkö: ")
while True:
try:
arvo = float(input("Anna muutettava arvo: "))
except ValueError:
print("Arvon tulee olla pelkkä luku")
else:
break
try:
kerroin = PITUUSKERTOIMET[yksikko]
except KeyError:
print("Valitsemaasi yksikköä ei voida muuntaa")
else:
print(f"{arvo:.3f} {yksikko} on {arvo * kerroin:.3f} m")
Tämä toimii, mutta näyttää epämiellyttävältä, koska sama muutos pitäisi tehdä kolmeen muuhunkin funktioon. Voi myös kuvitella, miten epäluettavalta koodi näyttäisi, jos samanlaisia kyselyjä pitäisi tehdä peräkkäin useampia! Sen sijaan, jos tämä koodipätkä siirretään
funktioon
, esimerkiksi nimellä kysy_arvo
, saataisiin kysely piilotettua tämän näköiseen koodinpätkään:def kysy_arvo():
while True:
try:
luku = float(input("Anna muutettava arvo: "))
except ValueError:
print("Arvon tulee olla pelkkä luku")
else:
break
return luku
Loppuun on tietenkin lisättävä
return
, jotta silmukan sisällä saatu luku-muuttujan
arvo
saadaan käyttöön muuallakin. Toisaalta, koska myös returnin kohtaaminen lopettaa silmukan ja samalla koko funktion suorituksen, voidaan break
-rivi korvata returnilla:def kysy_arvo():
while True:
try:
luku = float(input("Anna muutettava arvo: "))
except ValueError:
print("Arvon tulee olla pelkkä luku")
else:
return luku
Jos käytämme nyt tätä funktiota aiemmassa koodissa, tulos näyttää yhtä nätiltä kuin ennen tarkistuksen lisäämistä, tai ehkä jopa hieman paremmalta, koska kysy_arvo on kuvaava nimi sille, mitä funktiossa tapahtuu:
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()
yksikko = input("Anna muutettava yksikkö: ")
arvo = kysy_arvo()
try:
kerroin = PITUUSKERTOIMET[yksikko]
except KeyError:
print("Valitsemaasi yksikköä ei voida muuntaa")
else:
print(f"{arvo:.3f} {yksikko} on {arvo * kerroin:.3f} m")
Sama muutos voidaan tehdä myös muihin funktioihin. Toiminta on lähes sama: ainoastaan fahrenheit-muunnoksessa käytetty "Anna lämpötila: " -kehoite korvaantuu nyt kysy_arvo-funktion yleisemmällä "Anna muutettava arvo: " -kehoitteella.
Tuomiomme on elinkautinen¶
Toinen ohjelmamme käyttömukavuutta lisäävä tekijä olisi se, että ohjelman suoritus ei lopu yhden operaation jälkeen. Käyttäjä on nähnyt jonkin verran vaivaa päästäkseen siihen vaiheeseen asti, että on pystynyt tekemään haluamansa muunnoksen. Olisi siis kohteliasta, jos samalla vaivalla voisi tehdä useampia muunnoksia ilman, että tarvitsee toistaa kaikkia askeleita joka kerta. Tähän ei tarvita mitään uusia ohjelmointitemppuja. Aloitetaan pääohjelmasta, jonka jätimme aiemmin tämän näköiseksi:
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")
Ohjelman suoritus toistuvasti onnistuu varsin helposti, kun koodiin lisätään
while-silmukka
seuraavasti: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()
while True:
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")
Valitettavasti nyt käyttäjällä ei ole mitään mahdollisuutta lopettaa ohjelmaa ilman väkivaltaisia näppäinyhdistelmiä. Aikaisemmin ohjelman suoritus loppui itsestään, mutta nyt se täytyy toteuttaa koodissa erikseen. Valikkoon pitää lisätä siis toiminto, jolla ohjelma lopetetaan. Tälle toiminnolle voidaan valita oma kirjainlyhenne. Valitettavasti helpot eli sekä l (lopeta) että p (poistu) ovat jo käytössä. Otetaan käyttöön siis vaikka q (quittaa). Aiemmin opitun perusteella tiedämme, että
break
-lauseella pääsee ulos silmukasta
ja siten koko ohjelmasta, koska silmukan jälkeen ei ole yhtään koodiriviä. Sijoitetaan siis ehtorakenteeseen
uusi elif-haara
lopetusta varten, ja lisätään uusi toiminto myös ohjeisiin: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("(Q)uittaa")
print()
while True:
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()
elif valinta == "q" or valinta == "quittaa":
break
else:
print("Valitsemaasi toimintoa ei ole olemassa")
Nyt käyttäjää ei potkaista ohjelmasta ulos muunnoksen jälkeen:
Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi Mahdolliset toiminnot: (P)ituus (M)assa (T)ilavuus (L)ämpotila (Q)uittaa 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 yksikkö: yd Anna muutettava arvo: 12 12.000 yd on 10.973 m Tee valintasi:
Päävalikkoon palauttaminen pituusmuunnoskyselystä on tosin sekin hieman ikävää, jos käyttäjä olisi halunnut tehdä toisen pituusmuunnoksen. Samanlainen
toistorakenne
olisi siis hyvä sisällyttää muihinkin funktioihin
. On lisäksi päätettävä, kummasta syötteestä
- arvosta vai yksiköstä - luetaan, haluaako käyttäjä palata takaisin. Yksikön valinta lienee parempi vaihtoehto, koska muuten tarvitaan muutoksia kysy_arvo-funktioon. Sovitaan, että tyhjäksi jätetty yksikkö tarkoittaa päävalikkoon palaamista. Täällä ei ole valmista ehtorakennetta
johon lopetusehdon voisi sijoittaa, joten tehdään uusi.def pituus():
print("Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne")
print("Jättämällä yksikön tyhjäksi palaat päävalikkoon")
print("Tuuma (in tai \")")
print("Jalka (ft tai ')")
print("Jaardi (yd)")
print("Maili (mi)")
print()
while True:
yksikko = input("Anna muutettava yksikkö: ")
if not yksikko:
break
arvo = kysy_arvo()
try:
kerroin = PITUUSKERTOIMET[yksikko]
except KeyError:
print("Valitsemaasi yksikköä ei voida muuntaa")
else:
print(f"{arvo:.3f} {yksikko} on {arvo * kerroin:.3f} m")
Huomattavaa on myös se, että silmukan päättävä break-lause on nostettu yksinkertaisen
if not yksikko:
-ehtolauseen kera arvon kyselyn yläpuolelle. Tällä yksinkertaisella kahden rivin kombinaatiolla silmukka päättyy heti, kun käyttäjä antaa tyhjän syötteen.
.Osoitetaan toiminta:Tämä ohjelma muuntaa yhdysvaltalaisia yksiköitä SI-yksiköiksi Mahdolliset toiminnot: (P)ituus (M)assa (T)ilavuus (L)ämpotila (Q)uittaa Tee valintasi: p Valitse pituusyksikkö seuraavien joukosta syöttämällä suluissa annettu lyhenne Jättämällä yksikön tyhjäksi palaat päävalikkoon Tuuma (in tai ") Jalka (ft tai ') Jaardi (yd) Maili (mi) Anna muutettava yksikkö: mi Anna muutettava arvo: 10 10.000 mi on 16093.440 m Anna muutettava yksikkö: Tee valintasi: q
Tästä viimeistään ilmenee, että
break
(kuten myös return) päättää silmukan
suorituksen välittömästi. Edes meneillään olevaa kierrosta
ei suoriteta loppuun. Siksi siis arvo jää kysymättä. Alla vielä animaatio toisesta esimerkkiohjelmasta, jossa tyhjä syöte
lopettaa silmukan
.Aasinlista¶
Käytetään edellistä esimerkkiä aasinsiltana seuraavaan aiheeseen. Aiemmin väläytettiin sivulauseessa ideaa, että muunnettavan arvon ja yksikön voisi kysyä yhdelläkin rivillä. Käyttö voisi silloin näyttää tältä:
Anna muutettava arvo ja yksikkö: 12 yd 12.000 yd on 10.973 m
Oppimistavoitteet: Tässä osiossa opitaan, mikä tarkalleen ottaen on lista ja miten se liittyy aiemmin käsiteltyihin tietotyyppeihin. Lisäksi opitaan tekemään listoja käyttäjän syöttämistä merkkijonoista sekä käsittelemään prosessissa syntyviä ongelmatapauksia.
Hiusten halkomista¶
Miten
merkkijonosta
otetaan irti kaksi erillistä asiaa? Ratkaisu riippuu käyttökontekstista, mutta ehdottomasti yleisin tapa on käyttää merkkijonon split
-metodia
. Kyseessä on metodi, joka jakaa merkkijonon osiin perustuen argumentiksi
annettuun erottimeen. Erotin voi olla yksittäinen merkki tai merkkijono, ja se ei tule näkymään tuloksissa mukana. Esimerkiksi desimaaliluvun voisi jakaa kokonaisosaan ja desimaaliosaan käyttämällä erottimena pistettä:In [1]: "12.54".split(".")
Out[1]: ['12', '54']
Tarvittaessa splitille voidaan myös määritellä
valinnaisella argumentilla
, että jakoja tehdään maksimissaan tietty määrä. Tätä ominaisuutta voidaan käyttää erityisesti tiedostopäätteiden
erottamiseen tiedoston nimestä
. Tyypillisiä skenaarioita, joissa pisteitä esiintyy tiedoston nimessä voivat olla musiikkitiedostojen nimet (esim. P.H.O.B.O.S. - 03 - Wisdoom.ogg
) tai versionumeron sisältävät asennuspaketit (esim. Pythonin oma: python-3.8.1.exe
). Tällöin käytetään tosin rsplit
-metodia, joka toimii muuten samalla tavalla, mutta aloittaa splittaamisen alun sijaan lopusta.In [1]: tiedosto = "aasinsilta_2.5.7.zip"
In [2]: tiedosto.rsplit(".", 1)
Out[2]: ['aasinsilta_2.5.7', 'zip']
Tässä esimerkissä siis tehdään vain yksi jako, ja koska käytetty
metodi
on rsplit, jaot tehdään alkaen lopusta. Näin saadaan erotettua tiedoston pääte nimestä itsestään. Metodin palauttama
hakasuluilla ympäröity kokonaisuus tunnetaan myös listana
(eng. list). Sellainen on nähty aiemminkin, sillä myös dir-funktio tuottaa listan: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']
Listan
tunnistaa nimenomaan ympäröivistä hakasulkeista, samoin kuin merkkijonon
tunnistaa lainausmerkeistä tai sanakirjan
aaltosulkeista. Muutenkin listat ovat tunnetuista tietotyypeistä
läheisintä sukua sekä merkkijonoille että sanakirjoille. Merkkijonon tapaan listat ovat sekvenssityyppisiä
(ts. niissä on yksittäisiä asioita peräkkäin). Toisaalta taas sanakirjan tavoin listat ovat muuntuvia
tietorakenteita
. Tarkoituksenamme oli tuottaa tämän näköistä toimintaa:Anna muutettava arvo ja yksikkö: 12 yd 12.000 yd on 10.973 m
Voimme siis saada aikaan listan käyttäjän
syötteistä
split-metodilla
(jakamalla merkkijono kahtia tyhjän kohdalta:syote = input("Anna muutettava arvo ja yksikkö: ").split(" ")
Listojen lyhyt oppimäärä¶
Todella lyhyt oppimäärä listoista voidaan antaa kolmen sanan kuvauksella: järjestetty kokoelma arvoja. Jokainen näistä kolmesta sanasta kertoo yhden keskeisen asian listoista:
- Lista on järjestetty kokoelma arvoja: listassa olevat asiat ovat määrätyssä järjestyksessä, ja niihin voidaan viitata niiden sijainnin perusteella. Viittaus ilmaistaan etäisyytenä listan alusta (tutkitaan pian miten). Ohjelmoija voi järjestää listan sisällön uudestaan haluamallaan tavalla.
- Lista on järjestetty kokoelma arvoja: lista sisältää määrätyn joukon arvoja; niiden lukumäärä voi olla mitä tahansa nollasta ylöspäin, joskin jossain vaiheessa viimeistään muistin rajat tulevat vastaan. Kuten oikeassakin elämässä, kokoelmaan voidaan lisätä asioita, ja niitä voidaan myös sieltä poistaa.
- Lista on järjestetty kokoelma arvoja: lista sisältää arvoja(ts.objekteja) eli mitä tahansa aiemmin kohdattuja asioita voidaan sisällyttää listaan – myös listoja. Listan sisältämiä arvoja kutsutaan listanalkioiksi.
Kertauksena: sanakirja on (melkein) järjestämätön kokoelma arvoja - listan pääominaisuus niihin verrattuna on siis järjestyksen hallinta. Vaikka sanakirjassa on nykyään järjestys (lisäysjärjestys), sitä ei voi muuttaa. Syntaksin kannalta lista on hakasuluilla rajattu arvo, jonka sisällä olevat alkiot on erotettu toisistaan pilkuilla. Kuten funktioiden määrityksien ja kutsujen kanssa, myös listoja kirjoittaessa pilkkua seuraa aina välilyönti. Lista voi sisältää numeroita:
In [1]: tulokset = [12.54, 5.12, 38.14, 9.04]
tai vastaavasti
merkkijonoja
:In [2]: jasenet = ["Haruna", "Tomomi", "Mami", "Rina"]
tai vaikka
funktioita
, muistutuksena siitä, että myös funktiot ovat objekteja
:In [3]: funktiot = [max, abs, min, round]
Sanakirjat
voi myös tunkea listaan, ja listan määrittelyn voi jakaa usealle riivlle kuten sanakirjankin:In [4]: mittaukset = [
...: {"arvo": 4.64, "yksikko": "in"},
...: {"arvo": 13.54, "yksikko": "yd"}
...: ]
ja kuten mainittua, myös
listoja
voi laittaa listojen sisälle:In [5]: [tulokset, jasenet, funktiot, mittaukset]
Out[5]:
[[12.54, 5.12, 38.14, 9.04],
['Haruna', 'Tomomi', 'Mami', 'Rina'],
[<function max>, <function abs>, <function min>, <function round>],
[{'arvo': 4.64, 'yksikko': 'in'}, {'arvo': 13.54, 'yksikko': 'yd'}]]
Viimeisestä esimerkistä näkyy myös miltä listan sisällä olevat listat näyttävät. Merkittävänä erona merkkijonoihin listan aloitus- ja lopetusmerkit ovat eri merkit (eli hakasulku auki ja hakasulku kiinni). Tämän ansiosta on mahdollista aloittaa uusi lista listan määrittelyn sisällä uudella avaavalla hakasulkeella. Tässä on tietenkin huomioitava, että kaikki aloitetut listat pitää muistaa myös lopettaa. Muutoin saadaan aikaan sama lopputulos, kuin jos unohtaa sulkea yhden tai useamman sulun rivillä, jossa kutsutaan sisäkkäin useita funktioita.
Kaapin paikka¶
Monet kohtaavat ohjelmointiuransa alkuvaiheella seuraavanlaisen pohdinnan: jos on olemassa
muuttujia
tyyliin luku_1, luku_2 jne., voisiko näitä kenties luoda dynaamisesti siten, että koodi generoisi itse lisää muuttujia aina luku_n:ään asti? Lyhyt vastaus kysymykseen on: EI (pitkä vastaus: teoriassa on, mutta vaatii tarpeettoman nokkelia temppuja, joilla voi helposti sotkea koodinsa). Vastaava toiminallisuus kylläkin onnistuu, kysymys on vain väärä. Oikea kysymys kuuluu, millaisella ratkaisulla voidaan ylläpitää tietoa ennaltamäärämättömästä määrästä arvoja. Vastaus tähän on tietenkin lista
. Listaan voidaan varastoida N kpl arvoja
, ja ne ovat siellä järjestyksessä. Vertaa:In [1]: luku_1 = 13
In [2]: luku_2 = 6
In [3]: luku_3 = 24
tähän:
In [4]: luvut = [13, 6, 24]
Jälkimmäinen tapa on huomattavasti parempi, koska sitä käyttämällä tiettyyn arvoon (13, 6 tai 24) viittaava luku ei ole kiinni muuttujan nimessä. Miten tietty arvo saadaan sitten haettua listasta? Listassa
alkiot
ovat tietyssä järjestyksessä. Tätä järjestystä kuvaa alkion etäisyys listan alusta siten, että listan ensimmäisen alkion (esimerkissä arvo 13) etäisyys alusta on 0. Jos siis halutaan viitata eli osoittaa listan ensimmäiseen arvoon, käytetään viittaavana arvona numeroa 0. Tälle sijaintiin viittavalle numerolle on oma nimensä: indeksi
(eng. index). Se toimii hyvin pitkälti samalla tavalla kuin sanakirjan
avain
.Itse viittaus tapahtuu seuraavanlaisella syntaksilla:
In [5]: luvut[0]
Out[5]: 13
In [6]: luvut[1]
Out[6]: 6
In [7]: luvut[2]
Out[7]: 24
Hakasulkeilla
listan
perässä on sama erityismerkitys kuin avainhaulla: sisältöön osoittaminen
. Keskeinen ero sanakirjan avaimiin on se, että avaimet voivat olla mielivaltaisia muuntumattomia
arvoja, mutta listan indeksit ovat aina numerot nollasta listan pituus miinus yhteen asti (ts. 0,...,N-1). Tämä ei itse asiassa ole pelkästään listojen ominaisuus, vaan toimii myös mm. merkkijonoille
. Esimerkiksi jos halutaan tarkistella sanan alkukirjainta:In [1]: sana = "aasisvengaa"
Out[1]: sana[0]
'a'
Oli kyse sitten
listoista
tai merkkijonoista
, indeksien
on oltava kokonaislukuja. Liukuluku
, jonka desimaaliosa on nolla ei kelpaa. Tämä on oikeastaan ensimmäinen kerta, kun int-funktiota saattaa tarvita liukulukujen muuttamiseen kokonaisluvuiksi. Esimerkkitapauksena voisi olla vaikka skenaario, jossa listasta poimitaan alkioita
jollain matemagiikalla, jossa käytetään liukulukuja (kuten vaikka keskimmäinen indeksi listasta jonka pituus on pariton) Tällöin siis lopullinen tulos tulee muuttaa kokonaisluvuksi ennen kuin sillä voidaan poimia alkioita listasta. Indeksiosoitusta
voidaan nyt hyödyntää jatkamalla tämän toiminnallisuuden toteuttamista:Anna muutettava arvo ja yksikkö: 12 yd 12.000 yd on 10.973 m
Käyttämällä indeksillä osoittamista voidaan split-
metodin
tuottamasta listasta ottaa sen alkiot erikseen käsiteltäväksi, vaikkapa tallentamalla
ne uusiin muuttujiin
.syote = input("Anna muutettava arvo ja yksikkö: ").split(" ")
arvo = syote[0]
yksikko = syote[1]
Esitellään vielä toinen tapa ottaa splitin tulokset
muuttujiin
talteen:arvo, yksikko = input("Anna muutettava arvo ja yksikkö: ").split(" ")
Tämä tapa toimii tosin pääasiassa silloin, kun kaikki arvot halutaan ottaa talteen
merkkijonoina
. Nyt joudutaan siis joka tapauksessa vielä ylimääräisellä rivillä muuttamaan arvo merkkijonosta liukuluvuksi
. Tällä kertaa saadaan myös aikaan eri poikkeus
, jos käyttäjän syötteestä
ei saada split-metodilla
oikean pituista listaa
ulos:---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-21b56e44815b> in <module>()
----> 1 arvo, yksikko = input("Anna muutettava arvo ja yksikkö: ").split(" ")
ValueError: not enough values to unpack (expected 2, got 1)
Kun merkkijonoja jaetaan osiin, tuleekin muistaa aina tarkistaa, että jako tuottaa oikean määrän tuloksia. Jos siis käytettäisiin tätä tapaa kysyä sekä arvo että yksikkö, kyselyfunktioon pitäisi tehdä muutoksia. Tämä vaatii uuden exceptin lisäämistä try-rakenteeseen. Tässä ei ole juurikaan eroa siihen, miten
ehtorakenteeseen
voidaan lisätä mielivaltainen määrä elif-lauseita. Poikkeuksen käsittelyssä voi yhtälailla olla mielivaltainen määrä except-lauseita, ja kuten ehtorakenteen haaroista, myös niistä suoritetaan ensimmäinen johon koodin tuottama poikkeus sopii. Tämän lisäksi funktion
tulee tietenkin palauttaa
nyt kaksi arvoa yhden sijaan.def kysy_muutettava():
while True:
try:
muutettava = input("Anna muutettava arvo ja yksikkö: ").split(" ")
arvo = float(muutettava[0])
yksikko = muutettava[1]
except ValueError:
print("Arvon tulee olla pelkkä luku")
except IndexError:
print("Syötteessä tulee olla arvo ja yksikkö välilyönnillä toisistaan erotettuna")
else:
return arvo, yksikko
Kahden paluuarvon vuoksi itse
funktiokutsuakin
täytyy muokata. Samalla muutetaan funktion nimi hieman kuvaavammaksi. Funktiokutsu näyttäisi nyt muualla koodissa tältä:arvo, yksikko = kysy_muutettava()
Emme kuitenkaan ota tätä käyttöön, koska se rikkoisi aikaisemmin kehitetyn päävalikkoon palaamisen (asian voisi toki korjata useammalla tavalla). Tämä jääköön esimerkiksi siitä, miten tyypillisesti täytyy käsitellä poikkeuksia, kun käyttäjän syötteestä halutaan ottaa split-metodilla useita arvoja kerralla.
Kokoelmateos listoista¶
Seuraavaksi tavoitteena on tehdä komentoriviohjelma, joka soveltuu kokoelmien ylläpitämiseen. Esimerkin ohjelma on tarkoitettu levykokoelman käsittelyyn, mutta se on helposti muokattavissa johonkin muuhun tarkoitukseen. Ohjelman perusominaisuuksia ovat levyjen lisääminen, poistaminen ja tietojen muokkaaminen.
Oppimistavoitteet: Tässä osiossa opitaan lisää listoista ja erityisesti, miten niihin lisätään tavaraa. Opimme myös, että listat ovat muuntuvia, ja jopa senkin, mitä tämä tarkoittaa käytännössä. Uutena asiana tulevat myös kommenttirivit, joita voi käyttää koodin selkeyttämiseen.
Välikommentti¶
Tilanteissa, joissa oikeaa koodia ei ole vielä käytössä, on hyvä tehdä korvaavaa koodia, joka toimii muun koodin näkökulmasta samalla tavalla. Koodiin voitaisiin siis laittaa jo valmiiksi
funktiot
lataa_kokoelma
ja tallenna_kokoelma
. Jälkimmäinen voidaan jättää tyhjäksi (käyttäen pass-avainsanaa). Ensimmäisessä sen sijaan voidaan luoda kokoelmaa esittävä lista
, johon on sijoitettu muutamia levyjä malliksi. Näin päästään kokeilemaan ohjelman ominaisuuksia, vaikka oikeaa dataa ei olekaan saatavilla.Sovitaan kokoelmaan laitettavaksi perustietoja viiteen eri kenttään: artistin nimi, levyn nimi, kappaleiden määrä, kesto ja julkaisuvuosi. Koska kentillä on selkeät nimet, paras tapa esittää yksittäinen levy on
sanakirja
, joka näyttää esim. tältä:{
"artisti": "Monolithe",
"albumi": "Nebula Septem",
"kpl_n": 7,
"kesto": "49:00",
"julkaisuvuosi": 2018
}
Ennen kuin meillä on yhtään levyä kokoelmassa, sovittujen avainten muistamisen avuksi voidaan ottaa
kommenttirivit
. Kommentit ovat rivejä, jotka Python
ohittaa koodia suorittaessa. Niille voi siis kirjoittaa lisätietoa koodin lukijalle, eli esimerkiksi itselleen. Kommenteilla voi selittää vaikean näköisiä kohtia ohjelman toiminnassa. Niitä voi myös käyttää suunnittelussa: toteuttamattomiin kohtiin koodissa voi kirjoittaa lyhyen kuvauksen, mitä ko. kohdan on tarkoitus tehdä kun se on valmis. Tällä tavalla, jos pidemmässä projektissa ohittaa jonkin yksityiskohdan alkuvaiheessa, pystyy vielä lopussakin muistamaan, mitä siihen oli alunperin tarkoitus tehdä. Kommenteilla voi myös luoda itselleen muistutuksia siitä, mitä tietorakenteen
olisi tarkoitus sisältää:def lataa_kokoelma():
# avaimet:
# artisti, albumi, kpl_n, kesto, julkaisuvuosi
kokoelma = []
return kokoelma
Rivit, jotka alkavat #-merkillä, ovat kommenttirivejä. Kun Python kohtaa tämän merkin
merkkijonon
ulkopuolella, se lopettaa rivin tulkitsemisen siihen ja jatkaa seuraavalle. Ylläolevassa koodissa on siis ainoastaan kolme suoritettavaa koodiriviä funktion sisällä: funktion määrittelyrivi; rivi, joka luo uuden (tyhjän) listan; ja rivi, joka palauttaa
sen.Kommenteilla
on käyttönsä myös ohjelman virheitä ratkoessa. Laittamalla kommenttimerkin rivin eteen voi nimittäin jättää rivin suorituksen pois ilman, että sitä tarvitsee suorilta käsin poistaa (jos vaikka myöhemmin osoittautuu, että rivi olikin oikein). Tällä tavalla voi myös tilapäisesti ohittaa virheellisen rivin tarkistaakseen toimiiko loput ohjemasta oikein.Kommenttien sukulaisia ovat
dokumenttimerkkijonot
(docstring). Näiden käyttö on hieman standardoidumpaa kuin kommenttien. Dokumenttimerkkijono liitetään tyypillisesti funktioon
. Se on itse asiassa juuri se merkkijono
, jonka näet help-funktiolla tulkissa
. Siitä tulee vähintään ilmetä, mitä funktio tekee, millaisia argumentteja
sille pitää ja/tai voi antaa sekä mitä se palauttaa
. Dokumenttimerkkijono merkitään tyypillisesti kolminkertaisilla lainausmerkeillä ja sen tulee sijaita välittömästi funktion määrittelyrivin alla ennen ensimmäistäkään varsinaista koodiriviä.def lataa_kokoelma():
"""
Luo testikokoelman. Palauttaa listan, joka sisältää viiden avain-arvo-parin
sanakirjoja.
Sanakirjan avaimet vastaavat seuraavia tietoja:
"artisti" - artisti nimi
"albumi" - levyn nimi
"kpl_n" - kappaleiden määrä
"kesto" - kesto
"julkaisuvuosi" - julkaisuvuosi
"""
# avaimet:
# artisti, albumi, kpl_n, kesto, julkaisuvuosi
kokoelma = []
return kokoelma
Jos kirjoitetaan tämän alle
pääohjelmaan
rivi help(lataa_kokoelma)
nähdään seuraavanlainen tuloste:Help on function lataa_kokoelma in module __main__: lataa_kokoelma() Luo testikokoelman. Palauttaa listan, joka sisältää viiden avain-arvo-parin sanakirjoja. Sanakirjan avaimet vastaavat seuraavia tietoja: "artisti" - artisti nimi "albumi" - levyn nimi "kpl_n" - kappaleiden määrä "kesto" - kesto "julkaisuvuosi" - julkaisuvuosi
Tämä näyttää jo varsin tutulta.
Dokumenttimerkkijonojen
käyttö on äärimmäisen hyvä tapa viimeistään, kun koodia saattaa katsoa joku muukin kuin sinä – mukaanlukien tulevaisuuden sinä, joka ei välttämättä muista, mitä koodia kirjoittaessa on päässäsi liikkunut. Tällä kurssilla tämä pätee erityisesti lopputyöhön, koska niitä tarkistavat assistentit ihan manuaalisesti koodia lukemalla. Isommissa projekteissa dokumenttimerkkijonoja voi hyödyntää myös erillisten työkalujen kanssa. Yksi tällainen on Sphinx, jonka avulla oikealla tavalla dokumentoidusta koodista voi luoda näppärät manuaalisivut koodille.Lisätään koodiimme vielä
tynkäfunktio
tallenna_kokoelma ja laitetaan joitain levyjä lataa_kokoelma-funktion listaan
seuraavia vaiheita varten. Alla on esitetty vain osa testikokoelman määrityksestä, koska sanakirjoja sisältävän listan kirjoitus menee jo aika pitkäksi... kommentin kentistä voi jättää pois, koska sama informaatio löytyy jo dokumenttimerkkijonosta.def lataa_kokoelma():
"""
Luo testikokoelman. Palauttaa listan, joka sisältää viiden avain-arvo-parin
sanakirjoja.
Sanakirjan avaimet vastaavat seuraavia tietoja:
"artisti" - artisti nimi
"albumi" - levyn nimi
"kpl_n" - kappaleiden määrä
"kesto" - kesto
"julkaisuvuosi" - julkaisuvuosi
"""
kokoelma = [
{
"artisti": "Alcest",
"albumi": "Kodama",
"kpl_n": 6,
"kesto": "42:15",
"julkaisuvuosi": 2016
},
{
"artisti": "Canaan",
"albumi": "A Calling to Weakness",
"kpl_n": 17,
"kesto": "1:11:17",
"julkaisuvuosi": 2002
},
{
"artisti": "Deftones",
"albumi": "Gore",
"kpl_n": 11,
"kesto": "48:13",
"julkaisuvuosi": 2016
},
# katkaistaan tästä, koko esimerkin koodissa määritelty 8 lisää
]
return kokoelma
def tallenna_kokoelma(kokoelma):
"""
Tallentaa kokoelman, joskus tulevaisuudessa.
"""
pass
Luodaan seuraavaksi alustava
pääohjelma
, josta voi valita ohjelman perustoiminnot, sekä muutama uusi tynkäfunktio
:def lisaa(kokoelma):
pass
def poista(kokoelma):
pass
def tulosta(kokoelma):
pass
kokoelma = lataa_kokoelma()
print("Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista:")
print("(L)isää uusia levyjä")
print("(P)oista levyjä")
print("(T)ulosta kokoelma")
print("(Q)uittaa")
while True:
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "l":
lisaa(kokoelma)
elif valinta == "p":
poista(kokoelma)
elif valinta == "t":
tulosta(kokoelma)
elif valinta == "q":
break
else:
print("Valitsemaasi toimintoa ei ole olemassa")
tallenna_kokoelma(kokoelma)
Kokoelma ladataan ohjelman alussa ja (ainakin teoriassa, jos funktio tekisi jotain) tallennetaan lopussa. Tiedostoon on myös lisätty kustakin pääohjelmassa mainitusta funktiosta tynkä, jotta koodin voi suorittaa (jätetään dokumenttimerkkijonot pois, koska funktiot tehdään kohta).
Mutanttilistat kasvavat silmissä¶
Sanakirjasta
poiketen Listaan
ei voi lisätä arvoja
pelkästään määrittelemällä uuden indeksin
ja sijoittamalla siihen jotain. Alla esitetty ensin avain-arvo-parin lisäys sanakirjaan, ja sen jälkeen näytetty mitä tapahtuu jos samaa koittaa listalle:In [1]: sk = {"a": 1, "b": 2, "c": 3}
In [2]: sk["d"] = 4
In [3]: sk
Out[3]: {'a': 1, 'b': 2, 'c': 3, 'd': 4}
In [4]: numerot = [213, 12, 45]
In [5]: numerot[3] = 53
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-5-f353565de7fa> in <module>()
----> 1 numerot[3] = 53
IndexError: list assignment index out of range
Mikä mahtaa olla se hyväksytty tapa? Hyvin lyhyesti esitettynä listaan lisätään
alkioita
append
-nimisellä metodilla
, joka lisää uuden alkion listan loppuun. Lyhyestä selityksestä lyhyt esimerkki:In [1]: numerot = [213, 12, 45]
In [2]: numerot.append(34)
In [3]: print(numerot)
[213, 12, 45, 34]
Ero tulee pitkälti siitä, että listaan lisätyt arvot menevät indeksien mukaisessa järjestyksessä, joten lisätessä ei tarvitse tietää monenneksiko alkioksi uusi arvo päätyy. Sen sijaan sanakirjassa avaimet ovat mielivaltaisia, joten avain johon arvo sidotaan pitää määrittää erikseen - ts. Python ei pysty päättelemään mikä avaimen tulisi olla. Samaa sen sijaan on se, että lista on
muuntuva
tietotyyppi, joten sanakirjan tavoin tässä prosessissa ei synny uutta kopioita. Samaten jos useampi muuttuja viittaa samaan listaan, lisätty alkio ilmestyy niihin kaikkiin. Tämä on myös ensimmäinen kerta kurssilla kun luodaan uusi viittaus ilman sijoitusoperaattoria
.Vilkaistaan help-funktiolla append-metodia:
append(...) method of builtins.list instance L.append(object) -> None -- append object to end
Muistetaan, että näissä tyypillisesti nuolen oikealla puolella on
funktion
tai metodin
palauttaman
arvon
tyyppi
. Palautettava arvo on tässä None, joka on siitä erityinen, että se tarkoittaa käytännössä "ei mitään". Kyseessä on siis tyhjä objekti
, joka on yhtä suuri ainoastaan itsensä kanssa ja jonka totuusarvo
on sama kuin False. Funktiot ja metodit, joissa ei ole returnia lainkaan, tai joissa return-rivillä ei ole yhtään palautettavaa arvoa, palauttavat aina Nonen.Koska
listat
ovat luonteeltaan muuntuvia
, lisäys käyttäytyy samalla tavalla kuin sanakirjojen tapauksessa. Animaatio havainnollistaa tätä ilmiötä listojen suhteen.On olemassa myös tapa lisätä
listaan
asioita siten, että tuloksena syntyy uusi lista. +-operaattorilla kaksi listaa voidaan liittää yhteen:In [1]: numerot_1 = [1, 2, 3]
In [2]: numerot_2 = numerot_1
In [3]: numerot_2 = numerot_2 + [4]
In [4]: print(numerot_1, numerot_2)
[1, 2, 3] [1, 2, 3, 4]
Tätä näkee tehtävän erittäin harvoin, koska listojen turha kopiointi on kaikin puolin raskasta ja yleensä on huomattavasti käytännöllisempää, että kaikki ohjelman osat käsittelevät samaa listaa. Näin on meidän levykatalogiohjelmassammekin. Kun lisaa-
funktio
lisää kokoelmaan levyjä, lisätyt levyt näkyvät niin pääohjelmassa
kuin kaikkialla muuallakin, eikä tarvitse huolehtia siitä, että vahingossa käytettäisiin jossain kohdassa vanhentunutta kopiota listasta. Tästä syystä myöskään lisaa-funktion ei tarvitse palauttaa
listaa, koska se käsittelee sitä yhtä ja ainoaa muistissa olevaa listaa, joka on alunperin ladattu lataa_kokoelma-funktiossa.Näillä tiedoilla pystymme toteuttamaan lisaa-funktion sisuskalut.
def lisaa(kokoelma):
print("Täytä lisättävän levyn tiedot. Jätä levyn nimi tyhjäksi lopettaaksesi")
while True:
levy = input("Levyn nimi: ")
if not levy:
break
artisti = input("Artistin nimi: ")
kpl_n = kysy_luku("Kappaleiden lukumäärä: ")
kesto = kysy_aika("Kesto: ")
vuosi = kysy_luku("Julkaisuvuosi: ")
kokoelma.append({
"artisti": artisti,
"albumi": levy,
"kpl_n": kpl_n,
"kesto": kesto,
"julkaisuvuosi": vuosi
})
print("Levy lisätty")
Tämä
funktio
lisää levyjä, kunnes käyttäjä syöttää
tyhjän levyn nimen. Uuden opitun asian soveltaminen näkyy usealle riville jaetussa append-metodikutsussa
, jossa kokoelma-listaan
lisätään uutta levyä kuvaava sanakirja. Huomaa myös returnin puute funktiossa. Sitä ei tarvita lainkaan, koska funktio muuttaa listaa, joka on olemassa samanlaisena kaikkialla missä siihen viitataan. Koodissa esiintyy myös kaksi uutta funktiota. Näistä kysy_luku on hyvin läheistä sukua aiemmin nähdylle kysy_arvo-funktiolle:def kysy_luku(kysymys):
while True:
try:
luku = int(input(kysymys))
except ValueError:
print("Arvon tulee olla kokonaisluku")
else:
return luku
Eroja aiempaan on kaksi: ensinnäkin, koska tätä käytetään kappaleiden määrän ja vuosiluvun kysymiseen, on järkevämpää hyväksyä ainoastaan kokonaislukuja. Toiseksi olemme korvanneet input-funktion
argumenttina
olleen kysymysmerkkijonon muuttujalla
. Näin toimimalla voidaan samaa funktiota käyttää kaikkiin kysymyksiin, joissa halutaan käyttäjältä kokonaisluku. Kysymys tulee nyt funktiokutsun argumenttina. Tämä on erittäinen käytännöllinen temppu mihin tahansa ohjelmaan, jossa kysytään useita kertoja samanlaisia syötteitä
(kuten tässä kysytään useaan kertaan kokonaislukuja). Poikkeuksia
valvova try-rakenne tarvitsee kirjoittaa vain kerran, mutta kuitenkin käyttäjälle annettavaa ohjetta voidaan helposti vaihtaa kutakin syötettä paremmin kuvaavaksi vaihtamalla funktion argumenttia.Sen sijaan kysy_aika-funktio jätetään toistaiseksi tyngäksi. Aikasyötteen oikeamuotoisuuden tarkistusta mietitään myöhemmin.
def kysy_aika(kysymys):
return input(kysymys)
Nyt funktio toimii kivan näköisesti:
Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista: (L)isää uusia levyjä (P)oista levyjä (T)ulosta kokoelma (Q)uittaa Tee valintasi: l Täytä lisättävän levyn tiedot. Jätä levyn nimi tyhjäksi lopettaaksesi Levyn nimi: Lead and Aether Artistin nimi: Skepticism Kappaleiden lukumäärä: 12.5 Arvon tulee olla kokonaisluku Kappaleiden lukumäärä: 6 Kesto: 47:49 Julkaisuvuosi: 1997 Levy lisätty Levyn nimi: Tee valintasi: q
Armotonta pyöritystä¶
Seuraavaksi listat saavat kaverikseen uudenlaisia silmukoita, joiden avulla niitä voidaan käydään nätisti läpi. Tavoitteena on saada toteutettua yksi uusi toiminto: listojen tulostaminen.
Oppimistavoitteet: Tästä osiosta on tarkoitus oppia mitä ovat for-silmukat, ja miten ne liittyvät listoihin. Tutustumme myös uuteen merkkijonometodiin, jolla saadaan uskomaton määrä kauneutta tulostuksiin.
Läpijuoksu¶
Koska tulostaminen esittelee
for-silmukat
vähän lempeämmin, aloitamme siitä. Tarkoitus on siis täyttää tulosta-funktio
aiemmasta ohjelmastamme. Ensimmäinen kokeiltava asia olisi tietenkin tulostaa lista
ihan vain print-funktiolla.def tulosta(kokoelma):
print(kokoelma)
Jos tämä olisi näin helppoa, asialle tuskin olisi tarvinnut omistaa omaa väliotsikkoaan. Ja eihän se olekaan, koska lopputulos ei ole erityisen luettava (rivit katkottu 80 merkin kohdalta käsin):
Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista: (L)isää uusia levyjä (M)uokkaa levyjä (P)oista levyjä (J)ärjestä kokoelma (T)ulosta kokoelma (Q)uittaa Tee valintasi: t [{'artisti': 'Alcest', 'albumi': 'Kodama', 'kpl_n': 6, 'kesto': '0:42:15', 'julk aisuvuosi': 2016}, {'artisti': 'Canaan', 'albumi': 'A Calling to Weakness', 'kpl _n': 17, 'kesto': '1:11:17', 'julkaisuvuosi': 2002}, {'artisti': 'Deftones', 'al bumi': 'Gore', 'kpl_n': 11, 'kesto': '0:48:13', 'julkaisuvuosi': 2016}, {'artist i': 'Funeralium', 'albumi': 'Deceived Idealism', 'kpl_n': 6, 'kesto': '1:28:22', 'julkaisuvuosi': 2013}, {'artisti': 'IU', 'albumi': 'Modern Times', 'kpl_n': 13 , 'kesto': '47:14', 'julkaisuvuosi': 2013}, {'artisti': 'Mono', 'albumi': 'You A re There', 'kpl_n': 6, 'kesto': '1:00:01', 'julkaisuvuosi': 2006}, {'artisti': ' Panopticon', 'albumi': 'Roads to the North', 'kpl_n': 8, 'kesto': '1:11:07', 'ju lkaisuvuosi': 2014}, {'artisti': 'PassCode', 'albumi': 'Clarity', 'kpl_n': 13, ' kesto': '0:49:27', 'julkaisuvuosi': 2019}, {'artisti': 'Scandal', 'albumi': 'Hel lo World', 'kpl_n': 13, 'kesto': '53:22', 'julkaisuvuosi': 2014}, {'artisti': 'S lipknot', 'albumi': 'Iowa', 'kpl_n': 14, 'kesto': '1:06:24', 'julkaisuvuosi': 20 01}, {'artisti': 'Wolves in the Throne Room', 'albumi': 'Thrice Woven', 'kpl_n': 5, 'kesto': '42:19', 'julkaisuvuosi': 2017}]
ja reaktio:
Toivottavaa olisikin, että tulostus olisi enemmänkin jotain tämän näköistä:
1. Alcest - Kodama (2016) [6] [42:15] 2. Canaan - A Calling to Weakness (2002) [17] [1:11:17] 3. Deftones - Gore (2016) [11] [48:13] 4. Funeralium - Deceived Idealism (2013) [6] [1:28:22] 5. IU - Modern Times (2013) [13] [47:14] 6. Mono - You Are There (2006) [6] [1:00:01] 7. Panopticon - Roads to the North (2014) [8] [1:11:07] 8. PassCode - Clarity (2019) [13] [49:27] 9. Scandal - Hello World (2014) [13] [53:22] 10. Slipknot - Iowa (2001) [14] [1:06:24] 11. Wolves in the Throne Room - Thrice Woven (2017) [5] [42:19]
Eli jokainen levy omalla rivillään ilman ylimääräisiä hakasulkuja, aaltosulkuja tai lainausmerkkejä. Koska yksi print tuottaa perinteisesti yhden tulosterivin, voidaan ajatella, että on tarpeen
kutsua
print-funktiota
jokaiselle listan
alkiolle
erikseen. Aiemmin tässä materiaalissa opittiin, että toisto tehdään tyypillisesti toistorakenteilla
eli silmukoilla. Opimme kuitenkin vain while-silmukan
, jota tulee käyttää silloin, kun toistojen määrää ei voida määrittää ennen toiston aloittamista. Tässä tapauksessa toistojen määrä kuitenkin voidaan määrittää, sillä ohjelma voi laskea tulostettavien levyjen määrän ennen toistorakenteen aloittamista.Pythonissa on
for-silmukka
on työkalu tällaista tapausta varten. Se on tarkoitettu erityisesti listojen ja muiden sekvenssien
läpikäyntiin. Näille löytyy yhteisnimitys iteroitava (iterable), joka viittaa juuri siihen, että ne ovat objekteja
, joita voi käydä läpi. Listojen lisäksi näitä ovat merkkijonot
ja myöhemmin esiteltävät monikot
, sekä erinäiset erikoisobjektit, kuten generaattorit
ja enumerate-objektit
.Pythonin for-silmukka suorittaa sisällään olevat komennot jokaista läpikäytävää
alkiota
kohden. Usein sitä käytetään toistamaan jokin operaatio tai operaatioiden sarja jokaiselle listan
alkiolle. Se soveltuu siis hyvin siihen, että tulostetaan jokainen listan alkio print-funktiolla. Ennen kuin hypätään suoraan lopputulokseen, tutkitaan for-silmukan yksityiskohtia. Aloitetaan mahdollisimman yksinkertaisesta esimerkistä tulkissa:In [1]: elukoita = ["koira", "kissa", "orava", "mursu", "aasi", "laama"]
In [2]: for elain in elukoita:
...: print(elain)
...:
koira
kissa
orava
mursu
aasi
laama
Tämä ainakin osoittaa todeksi väittämän, että for-silmukka suorittaa sisältämänsä operaation jokaisen läpikäytävän listan alkion kohdalla. Varsinainen silmukka määritellään seuraavasti, aloittamalla rivi
avainsanalla
:for elain in elukoita:
Itse for-avainsanan lisäksi myös in-
operaattori
on kiinteä osa for-silmukan määrittelyä. Tämä luetaan: "jokaiselle elain
sarjassa elukoita
". Aivan kuten funktiossa
on nimetyt parametrit
, tässä esimerkissä on elain-muuttuja
, jota kutsuttakoon silmukkamuuttujaksi
, edustaa silmukan sisällä listan kulloinkin käsittelyssä olevaa alkiota
. Se siis saa arvot
"koira", "kissa", "orava", "mursu", "aasi" ja "laama" tässä järjestyksessä silmukan suorituksen aikana. Arvo vaihtuu jokaisella kierroksella.Animaation lopussa myös ilmenee, että
silmukkamuuttuja
ei lakkaa olemasta silmukan päätyttyä. Täten se eroaa selkeästi funktion
parametrista
, joka on ainoastaan olemassa funktion sisällä. Tälle ei varsinaisesti ole mitään yleisiä käyttötarkoituksia. Asia on sen sijaan hyvä pitää mielessä, koska muuten saattaa tapahtua yllätyksiä. Varmuuden vuoksi on järkevintä käyttää silmukkamuuttujille sellaisia nimiä, joita ei käytetä muualla saman funktion näkyvyysalueella
tai globaalisti
.Tässä vaiheessa pitäisi siis olla tiedossa miten
for-silmukka
määritellään, ja miten silmukkamuuttujan
avulla voidaan listassa
oleville alkiolle
tehdä asioita. Kokeillaan siis laittaa tulosta-funktioon
silmukka, jossa alkiot tulostetaan yksitellen.def tulosta(kokoelma):
for levy in kokoelma:
print(levy)
Tuloste näyttää nyt tältä:
{'artisti': 'Alcest', 'albumi': 'Kodama', 'kpl_n': 6, 'kesto': '0:42:15', 'julkaisuvuosi': 2016} {'artisti': 'Canaan', 'albumi': 'A Calling to Weakness', 'kpl_n': 17, 'kesto': '1:11:17', 'julkaisuvuosi': 2002} {'artisti': 'Deftones', 'albumi': 'Gore', 'kpl_n': 11, 'kesto': '0:48:13', 'julkaisuvuosi': 2016} {'artisti': 'Funeralium', 'albumi': 'Deceived Idealism', 'kpl_n': 6, 'kesto': '1:28:22', 'julkaisuvuosi': 2013} {'artisti': 'IU', 'albumi': 'Modern Times', 'kpl_n': 13, 'kesto': '47:14', 'julkaisuvuosi': 2013} {'artisti': 'Mono', 'albumi': 'You Are There', 'kpl_n': 6, 'kesto': '1:00:01', 'julkaisuvuosi': 2006} {'artisti': 'Panopticon', 'albumi': 'Roads to the North', 'kpl_n': 8, 'kesto': '1:11:07', 'julkaisuvuosi': 2014} {'artisti': 'PassCode', 'albumi': 'Clarity', 'kpl_n': 13, 'kesto': '49:27', 'julkaisuvuosi': 2019} {'artisti': 'Scandal', 'albumi': 'Hello World', 'kpl_n': 13, 'kesto': '53:22', 'julkaisuvuosi': 2014} {'artisti': 'Slipknot', 'albumi': 'Iowa', 'kpl_n': 14, 'kesto': '1:06:24', 'julkaisuvuosi': 2001} {'artisti': 'Wolves in the Throne Room', 'albumi': 'Thrice Woven', 'kpl_n': 5, 'kesto': '42:19', 'julkaisuvuosi': 2017}
Selkeästi parempi kuin aiemmin, mutta ei vielä näytä ihan samalta kuin aikaisemmissa kauniissa ajatuksissa.
Listat julki¶
Listojen
ja muiden tietorakenteiden
tulostaminen on aina oma ongelmansa, ja sen ratkaisu riippuu pitkälti siitä mitä ohjelmalla halutaan tehdä. Yksi vaihtoehtoja voimakkaasti rajoittava tekijä on listan pituus. Jos listan pituus vaihtelee, se rajoittaa huomattavasti sitä, miten se voidaan tulostaa. Meidän ohjelmamme tapauksessa itse kokoelman pituus voi vaihdella, minkä vuoksi tulostammekin sen silmukalla
. Listan sisältämien sanakirjojen käytettäköön tässä vaiheessa yksinkertaista printtiä.def tulosta(kokoelma):
for levy in kokoelma:
print(
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Tämä tuottaa melkein toivotun näköisen tulosteen, joskin eteen laitetut järjestysnumerot puuttuvat. Miten ne lisätään? Yksi looginen vaihtoehto olisi tehdä laskuri:
def tulosta(kokoelma):
numero = 1
for levy in kokoelma:
print(
f"{numero:2}. "
f'{levy["artisti"]}, {levy["albumi"]}, {levy["kpl_n"]}, '
f'{levy["kesto"]}, {levy["julkaisuvuosi"]}'
)
numero += 1
Eli otetaan kokonaislukumuuttuja, jolla on alkuarvona 1, ja lisätään siihen 1 jokaisella kierroksella. On kuitenkin olemassa nätimpi tapa saada
indeksit
käyttöön for-silmukan sisällä. Käytännössähän tuo järjestysnumero on sama kuin alkion indeksi listassa + 1. Kirjoitetaan koodirivi ensin ja ihmetellään sitä jälkeenpäin:def tulosta(kokoelma):
for i, levy in enumerate(kokoelma):
print(
f"{i + 1:2}. "
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Ensimmäinen kysymys on: mikä on tuo mystinen enumerate? Tyypilliseen tapaan asiaa voidaan tutkia
tulkissa
. Koska levylistassa on vähän liikaa tavaraa alkiota kohti, otetaan käyttöön aiemmin esitelty eläinlista.In [1]: elukoita = ["koira", "kissa", "orava", "mursu", "aasi", "laama"]
In [2]: enumerate(elukoita)
Out[2]: <enumerate at 0x7fb34458a5e8>
Tämähän oli hyödyllistä informaatiota. Sattumoisin enumerate tuottaa
generaattorimaisen
objektin, joita voidaan kyllä käydä läpi silmukalla, mutta eivät ole tietorakenteita
vaan funktioita, jotka tuottavat tietyn sekvenssin seuraavan arvon kun niitä kutsutaan. Generaattorin - ja enumeraten - voi kuitenkin purkaa listaksi, joten otetaan otto 2:In [3]: list(enumerate(elukoita))
Out[3]:
[(0, 'koira'),
(1, 'kissa'),
(2, 'orava'),
(3, 'mursu'),
(4, 'aasi'),
(5, 'laama')]
Listan sisällä esiintyvät sulkeissa olevat tietorakenteet ovat nimeltään
monikkoja
. Niistä ei ole vielä puhuttu, mutta eipä niissä ole paljon kerrottavaakaan: monikko on listan muuntumaton
sukulainen. Monikkoa siis luetaan kuin listaa, mutta sitä ei voi muokata. Jos monikkoja sisältävää listaa käydään läpi yhden muuttujan
silmukalla, kullakin kierroksella muuttuja saa arvoksi monikon. Silmukkamuuttujien kanssa voidaan kuitenkin tehdä sama temppu kuin tapauksessa, jossa funktiokutsu
palauttaa useita muuttujia. Eli kun tiedetään varmuudella, että listan jokaisessa alkiossa
on sisällä kaksi alkiota, se voidaan suoraan purkaa kahteen muuttujaan kuten kokoelmaesimerkissä jo tehtiin. Sama elukoilla:In [4]: for i, elain in enumerate(elukoita):
...: print(f"{i}. {elain}")
...:
0. koira
1. kissa
2. orava
3. mursu
4. aasi
5. laama
Tästä nähdään, että sekä i että elain saavat jokaisella silmukan
kierroksella
uudet arvot. Valittu silmukkamuuttujan
nimi i on hyvin tyypillinen tämäntapaisissa silmukoissa kaikissa ohjelmointikielissä. Jos silmukoita on sisäkkäin useampia, valitaan seuraavaksi puolestaan j, sitten k. Pidempien nimien käyttökin on toki sallittua.Alla on kaksi muuta tapaa kirjoittaa vastaava toiminallisuus, joka selventää millaisten kokonaisuuksien kanssa edellisen esimerkin toiminta on identtistä. Ensin tekemällä alkion purkaminen eri rivillä, mistä seuraa yksi ylimääräinen rivi:
In [5]: for alkio in enumerate(elukoita):
...: i, elain = alkio
...: print(f"{i}. {elain}")
Toiseksi käyttämällä
indeksiosoitusta
suoraan tulostusrivillä, joka taas tekee tulostusrivistä vähemmän selkeän:In [6]: for alkio in enumerate(elukoita):
...: print(f"{alkio[0]}. {alkio[1]}")
Listojen tulostukseen liittyy vielä yksi konsti, joka on erityisen kätevä tulostettaessa listojan joiden pituutta ei tiedetä ennalta, mutta kuitenkin halutaan tulostaa sen sisältö yhdelle riville. Esimerkkinä otettakoon vaikka ohjelma, johon käyttäjä
syöttää
sanoja. Sanat laitetaan listaan, ja yksi ohjelman toiminnoista on tulostaa syötetyt sanat. Koska syötettyjen sanojen lista voi olla miten pitkä tahansa, ei voida käyttää merkkijonon muotoilua, koska paikanpitimien
määrää ei voida tietää. Sen sijaan voidaan käyttää join
-nimistä merkkijonometodia
. Tämän käyttö on hieman erikoisen näköistä.Tarkalleen ottaen join-metodi toimii siten, että se liittää
listan
alkiot
yhteen merkkijonoksi käyttäen "liittimenä" merkkijonoa
. Tässä mielessä se on kuin käänteinen split. Kuten split, myös join on merkkijonon metodi, mistä johtuu myös hieman outo syntaksi
, jossa liittämiseen käytettävä merkkijono on rivillä ensin, ja liitettävä lista vasta metodikutsun
argumenttina. Syynä on todennäköisesti se, että join pystyy toimimaan minkä tahansa sekvenssityyppisen
objektin kanssa, mutta liittimen on aina oltava merkkijono - niinpä metodi on kiinnitetty siihen sen sijaan, että se toteutettaisiin erikseen jokaiselle sekvenssityypille.Kyseisen metodin käyttö listojen tulostamiseen on sen verran näppärä temppu, että sitä voi hyvin käyttää silloinkin kun listan pituus tiedetään. Sen lisäksi, että joinia käyttävä rivi on huomattavasti kompaktimpi, sen toiminta on myös dynaamisempi. Tästä johtuen muutokset muualla koodissa harvemmin johtavat yllättäviin virheisiin joinia käyttävässä tulostuksessa. Esimerkkiohjelmamme käyttää
sanakirjoja
, mutta teoriassa joinia voisi käyttää, jos haetaan sanakirjasta pelkät arvot listana values-metodilla:---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/joku/kansio/kokoelma.py in <module>()
77 poista(kokoelma)
78 elif valinta == "t":
---> 79 tulosta(kokoelma)
80 elif valinta == "q":
81 break
/joku/kansio/kokoelma.py in tulosta(kokoelma)
62 def tulosta(kokoelma):
63 for i, levy in enumerate(kokoelma):
---> 64 print(f"{:2}. {', '.join(levy.values())})
65
66 kokoelma = lataa_kokoelma()
TypeError: sequence item 0: expected str instance, int found
Tästä ilmenee tärkeä rajoitus:
listan
(tai vastaavan) kaikkien alkioiden
tulee olla merkkijonoja
, jotta join-metodia voidaan käyttää. Ratkaisuvaihtoehdot tässä tilanteessa ovat: a) käyttää jotain muuta menetelmää tai b) muuttaa kaikki listan alkiota merkkijonoiksi ennen join-metodin käyttöä. Mahdollista on myös sisällyttää alunperinkin kaikki numerot sanakirjoihin merkkijonoina, mutta silloin niillä ei voi enää suorittaa laskutoimituksia. Jälleen kerran riippuu hyvin paljon ohjelman tarkoituksesta, mitä menetelmää kannattaa käyttää.Esimerkkikoodissa käytettäköön siis tätä ratkaisua:
def tulosta(kokoelma):
for i, levy in enumerate(kokoelma):
print(
f"{i + 1:2}. "
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Uusi maailmanjärjestys¶
Toistaiseksi olemme saaneet aikaan tämän näköisen koodin levykatalogiohjelmallemme. Jos olet seurannut koodin kehitystä päivittämällä itsellesi materiaalin aikana tehtyjä muutoksia, pitäisi koneellasi olla samanlainen koodi. Ainoastaan funktioiden määrittelyjärjestys voi hieman erota.
def kysy_luku(kysymys):
while True:
try:
luku = int(input(kysymys))
except ValueError:
print("Arvon tulee olla kokonaisluku")
else:
return luku
def kysy_aika(kysymys):
return input(kysymys)
def lataa_kokoelma():
"""
Luo testikokoelman. Palauttaa listan, joka sisältää viiden avain-arvo-parin
sanakirjoja.
Sanakirjan avaimet vastaavat seuraavia tietoja:
"artisti" - artisti nimi
"albumi" - levyn nimi
"kpl_n" - kappaleiden määrä
"kesto" - kesto
"julkaisuvuosi" - julkaisuvuosi
"""
kokoelma = [
{
"artisti": "Alcest",
"albumi": "Kodama",
"kpl_n": 6,
"kesto": "42:15",
"julkaisuvuosi": 2016
},
{
"artisti": "Canaan",
"albumi": "A Calling to Weakness",
"kpl_n": 17,
"kesto": "1:11:17",
"julkaisuvuosi": 2002
},
{
"artisti": "Deftones",
"albumi": "Gore",
"kpl_n": 11,
"kesto": "48:13",
"julkaisuvuosi": 2016
},
# katkaistaan tästä, koko esimerkin koodissa määritelty 8 lisää
]
return kokoelma
def tallenna_kokoelma(kokoelma):
"""
Tallentaa kokoelman, joskus tulevaisuudessa.
"""
pass
def lisaa(kokoelma):
print("Täytä lisättävän levyn tiedot. Jätä levyn nimi tyhjäksi lopettaaksesi")
while True:
levy = input("Levyn nimi: ")
if not levy:
break
artisti = input("Artistin nimi: ")
kpl_n = kysy_luku("Kappaleiden lukumäärä: ")
kesto = kysy_aika("Kesto: ")
vuosi = kysy_luku("Julkaisuvuosi: ")
kokoelma.append({
"artisti": artisti,
"albumi": levy,
"kpl_n": kpl_n,
"kesto": kesto,
"julkaisuvuosi": vuosi
})
print("Levy lisätty")
def poista(kokoelma):
"""
Poistaa leven kokoelmasta, joskus tulevaisuudessa.
"""
pass
def tulosta(kokoelma):
for i, levy in enumerate(kokoelma):
print(
f"{i + 1:2}. "
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
kokoelma = lataa_kokoelma()
print("Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista:")
print("(L)isää uusia levyjä")
print("(P)oista levyjä")
print("(T)ulosta kokoelma")
print("(Q)uittaa")
while True:
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "l":
lisaa(kokoelma)
elif valinta == "p":
poista(kokoelma)
elif valinta == "t":
tulosta(kokoelma)
elif valinta == "q":
break
else:
print("Valitsemaasi toimintoa ei ole olemassa")
tallenna_kokoelma(kokoelma)
Koodin määrän puolesta tämä on noin kolmasosa~puolet lopputyöstä. Seuraavaksi lähdemme toteuttamaan lisäominaisuutta, joka johdattaa meidät uusiin listoilla tehtäviin asioihin: sisällön järjestämiseen ja sisällön leikkaamiseen. Lisätään siis ohjelmaan uusi funktio:
def jarjesta(kokoelma):
pass
Ja lisätään päävalikkoon vastaava toiminto muokkaamalla pääohjelmaa:
kokoelma = lataa_kokoelma()
print("Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista:")
print("(L)isää uusia levyjä")
print("(P)oista levyjä")
print("(J)ärjestä kokoelma")
print("(T)ulosta kokoelma")
print("(Q)uittaa")
while True:
valinta = input("Tee valintasi: ").strip().lower()
if valinta == "l":
lisaa(kokoelma)
elif valinta == "p":
poista(kokoelma)
elif valinta == "j":
jarjesta(kokoelma)
elif valinta == "t":
tulosta(kokoelma)
elif valinta == "q":
break
else:
print("Valitsemaasi toimintoa ei ole olemassa")
tallenna_kokoelma(kokoelma)
Osaamistavoitteet: Tutkimme miten listoja voidaan järjestää
sort
-metodilla ja sopivilla apufunktioilla. Lisäksi merkkijonojen järjestettävyydestä opitaan uusia asioita.Järjestystä, kiitos!¶
Kokoelma pitäisi saada siis järjestettyä. Tavoitteena on, että käyttäjä voi valita, minkä kentän perusteella kokoelma järjestetään ja onko järjestys nouseva vai laskeva (siis pienimmästä suurimpaan vai toisin päin). Kaikki tämä hoituu käyttämällä
listojen
sort-metodia
:In [1]: lista_1 = [37, 5, 12]
In [2]: lista_1.sort()
In [3]: lista_1
Out[3]: [5, 12, 37]
Tämä metodi järjestää listan
alkiot
nousevaan suuruusjärjestykseen, ts. pienin alkio ensin ja suurin viimeisenä. Luvuille tämä on helppo ymmärtää. Merkkijonoille järjestäminen perustuu aakkosjärjestykseen:In [1]: elukoita = ["mursu", "apina", "aasi", "laama", "koala", "aropupu", "hirvi"]
In [2]: elukoita.sort()
In [3]: elukoita
Out[3]: ['aasi', 'apina', 'aropupu', 'hirvi', 'koala', 'laama', 'mursu']
Listojen
keskinäinen järjestys perustuu puolestaan oletuksena ensimmäisten alkioiden vertailulle. Näiden ollessa samat, vertaillaan toista alkiota jne. Kokoelmamme sisältää kuitenkin sanakirjoja, joten järjestäminen ilman mitään lisämäärteitä aiheuttaa TypeError-poikkeuksen
. Tätä varten sort-metodissa
on valinnainen argumentti
key
. Tällä argumentilla voidaan määritellä funktio
, jolla kaikki alkiot
käsitellään ennen vertailua. Tarkastellaan ensin yksinkertaista tilannetta, jossa käytetään Pythonissa valmiiksi olevaa funktiota. Numeroita sisältävien merkkijonojen
järjestäminen aiheuttaa joskus kummallisia tuloksia:In [1]: luvut = ["2", "12", "5", "43", "48"]
In [2]: luvut.sort()
In [3]: luvut
Out[3]: ['12', '2', '43', '48', '5']
Jos tavoitteena oli saada lukuja esittävät merkkijonot niiden numeeriseen suuruusjärjestykseen, tulos ei ole toivottu. Asian voi korjata käyttämällä int-funktiota key-argumenttina:
In [4]: luvut.sort(key=int)
In [5]: luvut
Out[5]: ['2', '5', '12', '43', '48']
Tämä on ensimmäinen tilanne tässä materiaalissa, kun
funktion
nimeä käytetään oikeassa koodissa ilman, että perässä olisi kutsumista
merkitsevät sulut. Tämä on erityinen mekanismi ohjelmoinnissa, jota kutsutaan kutsupyynnöksi
. Tässä mekanismissa funktion suoran kutsumisen sijaan kerrotaan jollekin toiselle ohjelman osalle, mitä funktiota sen tulee kutsua. Jotta kutsupyyntö voi toimia, funktion tulee vastata argumenttien lukumäärältä ja tyypeiltä sitä, mitä funktiota käyttävä ohjelman osa vaatii.Tässä tapauksessa kutsuttava ohjelman osa on sort-metodi, jonka key-argumentilla voidaan määrittää kutsupyyntö. Metodi kutsuu tähän parametriin annettua funktiota siinä vaiheessa, kun se tarvitsee
vertailuarvon
kustakin listan alkiosta. Tässä tapauksessa funktion tulee olla sellainen, joka saa yhden argumentin ja palauttaa
yhden arvon. Meidän tarkoitukseemme ei tosin sovi mikään valmis funktio. Tarvitsemme nimittäin funktion, joka valitsee yhden kentän edustamaan koko sanakirjaa.Koska key-argumenttina olevaa funktiota kutsutaan tasan yhdellä argumentilla, funktiolle ei voi välittää tietoa siitä, mikä kenttä tulisi valita. Ainoa tuntemamme tapa sisällyttää tämä tieto on siis tehdä jokaista kenttää varten oma funktionsa. Ei kovin optimaalista, mutta valitettavasti pakollista tämän materiaalin antamien tietojen puitteissa. Parempiin keinoihin voi perehtyä Pythonin dokumentaatiota lukemalla. Nämä apufunktiot, joita tulemme omassa koodissamme antamaan key-argumenteiksi, ovat varsin yksinkertaisia:
def valitse_artisti(levy):
return levy["artisti"]
Funktio palauttaa siis kustakin levystä sen "artisti"-avainta vastaavan kentän käytettäväksi sort-metodin vertailussa. Vastaavasti muut neljä:
def valitse_albumi(levy):
return levy["albumi"]
def valitse_kpl_n(levy):
return levy["kpl_n"]
def valitse_kesto(levy):
return levy["kesto"]
def valitse_julkaisuvuosi(levy):
return levy["julkaisuvuosi"]
Itse jarjesta-funktio noudattaa pitkälti tuttua kaavaa, eli siinä kysytään, minkä kentän mukaan halutaan järjestää, ja sitten
ehtorakenteessa
valitaan oikea järjestystapa:def jarjesta(kokoelma):
print("Valitse kenttä jonka mukaan kokoelma järjestetään syöttämällä kenttää vastaava numero")
print("1 - artisti")
print("2 - levyn nimi")
print("3 - kappaleiden määrä")
print("4 - levyn kesto")
print("5 - julkaisuvuosi")
kentta = input("Valitse kenttä (1-5): ")
if kentta == "1":
kokoelma.sort(key=valitse_artisti)
elif kentta == "2":
kokoelma.sort(key=valitse_albumi)
elif kentta == "3":
kokoelma.sort(key=valitse_kpl_n)
elif kentta == "4":
kokoelma.sort(key=valitse_kesto)
elif kentta == "5":
kokoelma.sort(key=valitse_julkaisuvuosi)
else:
print("Kenttää ei ole olemassa")
Koska olemme kaukaa viisaasti päättäneet käyttää nimenomaan numeroita kappaleiden määrän tallentamiseen, niidenkin osalta järjestäminen tapahtuu oikein. Kesto sen sijaan tuottaa hieman ongelmallisen näköisiä tuloksia:
1. Mono, You Are There, 6, 1:00:01, 2006 2. Slipknot, Iowa, 14, 1:06:24, 2001 3. Panopticon, Roads to the North, 8, 1:11:07, 2014 4. Canaan, A Calling to Weakness, 17, 1:11:17, 2002 5. Funeralium, Deceived Idealism, 6, 1:28:22, 2013 6. Alcest, Kodama, 6, 42:15, 2016 7. Wolves in the Throne Room, Thrice Woven, 5, 42:19, 2017 8. IU, Modern Times, 13, 47:14, 2013 9. Deftones, Gore, 11, 48:13, 2016 10. PassCode, Clarity, 13, 49:27, 2019 11. Scandal, Hello World, 13, 53:22, 2014
Järjestys on muutoin pienimmästä suurimpaan, mutta kaikki yli tunnin levyt ovat sort-
metodin
mielestä lyhyempiä kuin alle tunnin levyt. Tämä johtuu tietenkin tutusta ongelmasta: yli tunnin kestävät levyt alkavat merkillä "1", joka on pienempi kuin muut numerot – paitsi nolla. Järjestämisen kannalta olisikin parempi, jos kaikissa kestoissa olisi merkittynä tunnit silloinkin, kun levy kestää alle tunnin. Samaisesta syystä ohjelmoijat rakastavat päivämääriä, jotka ovat muodossa vuosi-kuukausi-päivä - ne nimittäin järjestyvät oikeaan aikajärjestykseen suoraan sortilla, mikäli vain kaikissa numeroissa on etunollat (esim. 2015-07-22).Ensimmäinen askel on tietenkin lisätä nuo nollatunnit lataa_kokoelma-funktion tuottamaan mallikokoelmaan.
def lataa_kokoelma():
"""
Luo testikokoelman. Palauttaa listan, joka sisältää viiden avain-arvo-parin
sanakirjoja.
Sanakirjan avaimet vastaavat seuraavia tietoja:
"artisti" - artisti nimi
"albumi" - levyn nimi
"kpl_n" - kappaleiden määrä
"kesto" - kesto
"julkaisuvuosi" - julkaisuvuosi
"""
kokoelma = [
{
"artisti": "Alcest",
"albumi": "Kodama",
"kpl_n": 6,
"kesto": "0:42:15",
"julkaisuvuosi": 2016
},
{
"artisti": "Canaan",
"albumi": "A Calling to Weakness",
"kpl_n": 17,
"kesto": "1:11:17",
"julkaisuvuosi": 2002
},
{
"artisti": "Deftones",
"albumi": "Gore",
"kpl_n": 11,
"kesto": "0:48:13",
"julkaisuvuosi": 2016
},
# katkaistaan tästä, koko esimerkin koodissa määritelty 8 lisää
]
return kokoelma
Tämä ei kuitenkaan vielä takaa mitään, sillä käyttäjä voi levyjä
syöttäessään
jättää tunnit pois. Tällaisten korjaaminen pitäisi ylipäänsäkin olla koodin eikä käyttäjän tehtävä. Meillä on sopivasti olemassa jo funktio
, jota käytetään kestojen syöttämiseen. Toistaiseksi siellä ei tosin ole ollut erityisesti sisältöä. Lisätään nyt funktioon kaksi asiaa:- Tarkistus, että käyttäjän syöte on oikeanlainen.
- Nollatuntien lisäys tarvittaessa.
Tämä onnistuu, kun käyttäjän syöte splitataan kaksoispisteen kohdalta ja tutkitaan saatuja osia erikseen. Oletetaan, että kaikki levyt ovat alle 10 tuntia, joten yksi nolla tunteihin riittää, eikä etunollia siten tarvita yli tunnin levyihin.
def kysy_aika(kysymys):
while True:
osat = input(kysymys).split(":")
if len(osat) == 3:
h, min, s = osat
elif len(osat) == 2:
min, s = osat
h = "0"
else:
print("Anna aika muodossa tunnit:minuutit:sekunnit tai minuutit:sekunnit")
continue
try:
h = int(h)
min = int(min)
s = int(s)
except ValueError:
print("Aikojen on oltava kokonaislukuja")
continue
if not (0 <= min <= 59):
print("Minuuttien on oltava välillä 0-59")
continue
if not(0 <= s <= 59):
print("Sekuntien on oltava välillä 0-59")
continue
if h < 0:
print("Tuntien on oltava positiivinen kokonaisluku")
continue
return f"{h}:{min:02}:{s:02}"
Yhtäkkiä fuktio onkin aika pitkä. Pituutta lisäävät erityisesti yksittäisten osien tarkistukset: täytyy varmistaa että ne ovat lukuja ja lisäksi tarkistaa, että ne ovat oikeita kellonaikoja.
Funktiossa esiintyy myös uusi
avainsana
continue
. Siinä missä break
lopettaa silmukan
suorituksen, continue lopettaa meneillään olevan kierroksen
ja hyppää uuden kierroksen alkuun. Eli heti, jos törmätään johonkin tarkistukseen, jota käyttäjän syöte ei läpäise, palataan kysymään uutta. Tässä continueta on käytetty, koska muuten kaikkien tarkistusten tulisi olla toistensa sisällä, mistä seuraisi hyvin hankalan näköinen ohjausrakenne
. Lisäselvennystä continuen toiminnasta antaa animaatio:Huomaa myös, että return on tällä kertaa
while-silmukan
lopussa. Kun nimittäin löydetään sellainen syöte
, joka läpäisee kaikki testit, se voidaan palauttaa
ja samalla lopettaa silmukan läpikäyminen (koska funktio, joka sisältää sen, päättyy returniin).Yleisesti ottaen
continue
on suhteellisen harvoin käytettävä lause silmukoissa
. Sitä tarvitsee oikeastaan vain juuri tämän kaltaisissa tilanteissa, missä tehdään useita tarkistuksia, joista mikä tahansa voi aiheuttaa seuraavaan kierrokseen jatkamisen, oli kyse sitten syötteen kysymisestä uudestaan while-silmukassa
tai for-silmukan
tapauksessa siirtymisestä seuraavan alkion
käsittelyyn. Huomattavaa on, että continuen käytön voi aina korvata sisäkkäisillä ehtorakenteilla
. Kyseessä on siis vain kätevä työkalu, jolla tietynlaisiin silmukoihin saadaan hieman selkeämpää koodia.Kaikkien näiden muutosten jälkeen kokoelmaan voidaan nyt lisätä levyjä, joiden kesto on alle tunnin, ja keston mukaan järjestäminen toimii edelleen.
Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista: (L)isää uusia levyjä (M)uokkaa levyjä (P)oista levyjä (J)ärjestä kokoelma (T)ulosta kokoelma (Q)uittaa Tee valintasi: l Täytä lisättävän levyn tiedot. Jätä levyn nimi tyhjäksi lopettaaksesi Levyn nimi: Rotten Tongues Artistin nimi: Curse Upon a Prayer Kappaleiden lukumäärä: 9 Kesto: 43:17 Julkaisuvuosi: 2015 Levyn nimi: Tee valintasi: j Valitse kenttä jonka mukaan kokoelma järjestetään syöttämällä kenttää vastaava numero 1 - artisti 2 - levyn nimi 3 - kappaleiden määrä 4 - levyn kesto 5 - julkaisuvuosi Valitse kenttä (1-5): 4 Tee valintasi: t 1. Alcest, Kodama, 6, 0:42:15, 2016 2. Wolves in the Throne Room, Thrice Woven, 5, 0:42:19, 2017 3. Curse Upon a Prayer, Rotten Tongues, 9, 0:43:17, 2015 4. IU, Modern Times, 13, 0:47:14, 2013 5. Deftones, Gore, 11, 0:48:13, 2016 6. PassCode, Clarity, 13, 0:49:27, 2019 7. Scandal, Hello World, 13, 0:53:22, 2014 8. Mono, You Are There, 6, 1:00:01, 2006 9. Slipknot, Iowa, 14, 1:06:24, 2001 10. Panopticon, Roads to the North, 8, 1:11:07, 2014 11. Canaan, A Calling to Weakness, 17, 1:11:17, 2002 12. Funeralium, Deceived Idealism, 6, 1:28:22, 2013
Esimerkistä myös nähdään, että ohjelma lisää nollatunnit oikein.
Lopulta toteutamme vielä mahdollisuuden vaihtaa järjestys nousevasta laskevaksi. Tämä onnistuu sort-
metodin
toisella valinnaisella argumentilla
. Metodilla on nimittäin valinnainen argumentti reverse
, joka saa oletuksena
arvon False. Halutessamme voimme asettaa sen Trueksi, jolloin järjestäminen tehdään käänteisesti. Tarvitsee siis vain lisätä jarjesta-funktioon kysymys, haluaako käyttäjä järjestyksen nousevana vai laskevana.def jarjesta(kokoelma):
print("Valitse kenttä jonka mukaan kokoelma järjestetään syöttämällä kenttää vastaava numero")
print("1 - artisti")
print("2 - levyn nimi")
print("3 - kappaleiden määrä")
print("4 - levyn kesto")
print("5 - julkaisuvuosi")
kentta = input("Valitse kenttä (1-5): ")
jarjestys = input("Järjestys; (l)askeva vai (n)ouseva: ").lower()
if jarjestys == "l":
kaanna = True
else:
kaanna = False
if kentta == "1":
kokoelma.sort(key=valitse_artisti, reverse=kaanna)
elif kentta == "2":
kokoelma.sort(key=valitse_levy, reverse=kaanna)
elif kentta == "3":
kokoelma.sort(key=valitse_n, reverse=kaanna)
elif kentta == "4":
kokoelma.sort(key=valitse_kesto, reverse=kaanna)
elif kentta == "5":
kokoelma.sort(key=valitse_vuosi, reverse=kaanna)
else:
print("Kenttää ei ole olemassa")
Kysytään siis toinen
syöte
, ja sen perusteella asetetaan reverse-argumentiksi annettavan muuttujan
arvo Trueksi tai Falseksi. Tällä kertaa mikä tahansa syöte joka ei ole l tai L asettaa järjestyksen nousevaksi, koska se on oletus.Tulostusten kauneusleikkaus¶
Esitellään tässä viimeisessä osiossa vielä, miten listojen leikkaamisia voi hyödyntää tulosteen karsimisessa. Varsinkin jos kokoelma kasvaa suurempiin mittoihin, tulosteen selailu voi olla rasittavaa. Kehitellään alkeellinen karsimisratkaisu, joka näyttää 20 tulosta kerrallaan ja seuraavat 20 sitten, kun käyttäjä painaa enteriä, jne. Kaunistellaan tulostetta myös hieman.
Osaamistavoitteet: Tämän osion jälkeen pitäisi olla käsitys siitä, miten listojen leikkauksia käytetään. Lisäksi tutustumme for-silmukan erikoistapaukseen, jota käytetään toistamaan silmukassa oleva koodi tietty määrä kertoja.
Lista tulostuu pätkittäin¶
Tarkoitus on siis tulostaa kokoelma-listasta aina 20 levyä kerrallaan ja sitten jäädä odottamaan, että käyttäjä painaa enteriä. Lähdetään liikkeelle helpoimmasta vaatimuksesta, eli miten otetaan
listasta
ulos 20 ensimmäistä alkioita
. Esimerkin lyhyyden nimissä tosin käytämme pienempiä numeroarvoja ja listojen pituuksia. Otetaan siis tutuksi tulleesta elukkalistasta ensimmäiset kolme eläintä:In [1]: elukoita = ["mursu", "apina", "aasi", "laama", "koala", "aropupu", "hirvi"]
In [2]: top3 = elukoita[:3]
In [3]: top3
Out[3]: ['mursu', 'apina', 'aasi']
Varsinainen uusi asia on toisella rivillä. Merkintä
[:3]
tarkoittaa leikkausta
, joka alkaa listan alusta ja päättyy ennen indeksiä
3. Kaksoispisteen vasemmalla puolella on ensimmäisen leikkaukseen mukaan tulevan alkion indeksi. Sen puuttuessa oletetaan paikalle 0. Kaksoispisteen oikealla puolestaan on vuorostaan ensimmäisen leikkauksen ulkopuolelle jäävän alkion indeksi. Sen puuttuessa otetaan mukaan kaikki listan loput alkiot. Sama tulos saataisiin siis myös merkinnällä [0:3]
.Leikkauksissa positiivista on se, että niitä ei haittaa listan rajojen ylitys:
In [4]: elukoita[10:15]
Out[4]: []
Samalla nähdään esimerkki leikkauksesta, joka alkaa muualta kuin listan alusta. Leikkausta käyttämällä voimme muokata tulosta-funktion tulostamaan vain 20 ensimmäistä levyä kokoelmasta:
def tulosta(kokoelma):
for i, levy in enumerate(kokoelma[:20]):
print(
f"{i + 1:2}. "
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Lukuja kantamalla¶
Seuraavien 20
alkion
tulostamiseksi tarvitaan kaksi asiaa:- Pitää selvittää kuinka monta 20 levyn listausta pitää tulostaa (viimeinen voi olla vajaa)
- Pitää tehdä silmukka, joka käydään läpi näin monta kertaa.
Aloitetaan ensimmäisestä ongelmasta. Listan pituuden saa len-funktiolla, mistä voidaan jakolaskun keinoin selvittää, montako 20 alkion kokonaisuutta siitä löytyy. Ongelmaksi jää pyöristys oikeaan tulostusten määrään. Jos levyjä on 20 riittää yksi tulostus, mutta jos niitä on 21 pitää tulostuksia tehdä kaksi. Tarvitaan siis funktio, joka pyöristää aina ylöspäin. Tähän löytyy vastauksena math-
moduulin
ceil-funkio:def tulosta(kokoelma):
tulostuksia = math.ceil(len(kokoelma) / 20)
Tätä laskettua tietoa voidaan käyttää hyväksi uuden tulostussilmukan kehittelyssä.
Silmukkaa
tulee siis toistaa laskettu määrä kertoja, ja kullakin kierroksella
tulostetaan 20 levyä. Silmukka, joka suorittaa sisältönsä määrätyn määrän kertoja, tehdään tyypillisesti tietynlaisella for-silmukalla
. Siinä läpikäytävä (eli in-operaattorin oikealla oleva) arvo ei ole mikään olemassaoleva lista
, vaan silmukkaa varten luotu range
-objekti. Tämä on objekti
, joka tuottaa sarjan lukuja halutulta väliltä:In [1]: luvut = range(10)
In [2]: for luku in luvut:
...: print(luku)
...:
0
1
2
3
4
5
6
7
8
9
Luonnollisesti, jos käydään läpi luvut [0, ..., 9], saadaan aikaan kymmenen toistoa. Koodissa range-objekti voidaan laittaa myös suoraan for-silmukan määrittelyriville:
In [1]: for luku in range(10):
...: print(luku)
...
0
1
2
3
4
5
6
7
8
9
Vähemmän yllättäen range-funktion
argumenttina
oleva 10 voidaan myös korvata muuttujalla
. Tätä tietoa soveltamalla uusi tulosta-funktio alkaa muotoutua:PER_SIVU = 20
# välistä leikattu muut funktiot
def tulosta(kokoelma):
tulostuksia = math.ceil(len(kokoelma) / PER_SIVU)
for i in range(tulostuksia):
alku = i * PER_SIVU
loppu = (i + 1) * PER_SIVU
muotoile_sivu(kokoelma[alku:loppu])
Toiminta perustuu siihen, että lasketaan
listasta
uusi leikkaus
, jonka aloituspiste on kierroksen
numero (ensimmäinen kierros on 0) kerrottuna 20:llä ja lopetuspiste kierroksen numero + 1 kerrottuna 20:llä. Näin siis saadaan kätevästi leikkaukset 0:20, 20:40, 40:60 jne. Luku 20 on sijoitettu ohjelman alkuun laitettavaan vakioon
, jotta koodia on helpompi muuttaa jälkikäteen. Funktiossa
kutsuttavaan
muotoile_sivu-funktioon voidaan itse asiassa kopioida tulosta-funktion vanha sisältö sellaisenaan. Vaihdetaan tosin kokoelma-parametrin
nimi kuvaavampaan.def muotoile_sivu(rivit):
for i, levy in enumerate(rivit):
print(
f"{i + 1:2}. "
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Tällä hetkellä toiminta ei tosin eroa mitenkään aiemmasta, sillä tulostuksesta puuttuu pysähtyminen 20 tulostettavan levyn välein. Muistamme kuitenkin, että input-funktio keskeyttää ohjelman suorituksen kunnes käyttäjä antaa
syötteen
. Tätä tietoa voidaan hyödyntää nytkin:def tulosta(kokoelma):
tulostuksia = math.ceil(len(kokoelma) / PER_SIVU)
for i in range(tulostuksia):
alku = i * PER_SIVU
loppu = (i + 1) * PER_SIVU
muotoile_sivu(kokoelma[alku:loppu])
if i < tulostuksia - 1:
input(" -- paina enter jatkaaksesi tulostusta --")
Syöteriviä edeltävä
if-lause
jättää input-funktiokutsun pois kun ollaan päästy viimeisen tulostettavan levyryhmän kohdalle. Alla on esitetty toiminta muuttamalla tulostettavien määrä viideksi (kätevästi PER_SIVU-vakion
arvoa muuttamalla!), koska esimerkkikokoelmassamme ei ole yli 20 levyä.Tämä ohjelma ylläpitää levykokoelmaa. Voit valita seuraavista toiminnoista: (L)isää uusia levyjä (M)uokkaa levyjä (P)oista levyjä (J)ärjestä kokoelma (T)ulosta kokoelma (Q)uittaa Tee valintasi: t 1. Alcest, Kodama, 6, 0:42:15, 2016 2. Canaan, A Calling to Weakness, 17, 1:11:17, 2002 3. Deftones, Gore, 11, 0:48:13, 2016 4. Funeralium, Deceived Idealism, 6, 1:28:22, 2013 5. IU, Modern Times, 13, 47:14, 2013 -- paina enter jatkaaksesi tulostusta -- 1. Mono, You Are There, 6, 1:00:01, 2006 2. Panopticon, Roads to the North, 8, 1:11:07, 2014 3. PassCode, Clarity, 13, 49:27, 2019 4. Scandal, Hello World, 13, 53:22, 2014 5. Slipknot, Iowa, 14, 1:06:24, 2001 -- paina enter jatkaaksesi tulostusta -- 1. Wolves in the Throne Room, Thrice Woven, 5, 42:19, 2017 Tee valintasi:
Ihan vielä ei mennyt putkeen, koska levyjen järjestysnumerot alkavat nyt joka "sivulla" uudestaan ykkösestä. Tätä varten täytyy vielä salakuljettaa toinen
argumentti
muotoile_sivu-funktiolle: tulostettavan sivun numero.def muotoile_sivu(rivit, sivu):
for i, levy in enumerate(rivit):
print(
f"{i + 1 + sivu * PER_SIVU:2}. ",
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Tosin tuon indeksimatikan voisi laittaa myös toiseen paikkaan. Enumeratelle kelpaa nimittäin toinenkin argumentti, joka kertoo mistä numerosta numerointi lähtee liikkeelle:
def muotoile_sivu(rivit, sivu):
for i, levy in enumerate(rivit, sivu * PER_SIVU + 1):
print(
f"{i:2}. ",
f"{levy['artisti']}, {levy['albumi']}, {levy['kpl_n']}, "
f"{levy['kesto']}, {levy['julkaisuvuosi']}"
)
Tässä muutos on vielä aika yhdentekevä, mutta jos silmukassa käytettäisiin järjestysnumeroa useammassa kuin yhdessä paikassa, edut ovat aika ilmeisiä. Nyt tarvitsee vielä antaa tälle uudelle sivu-
parametrille
argumentti funktiota kutsuttaessa:def tulosta(kokoelma):
tulostuksia = math.ceil(len(kokoelma) / PER_SIVU)
for i in range(tulostuksia):
alku = i * PER_SIVU
loppu = (i + 1) * PER_SIVU
muotoile_sivu(kokoelma[alku:loppu], i)
if i < tulostuksia - 1:
input(" -- paina enter jatkaaksesi tulostusta --")
Yhteen asiaan ei tässä ole sen kummemmin kiinnitetty huomiota, koska sen pitäisi olla tässä vaiheessa selvää, mutta... Huomasitko kuinka molemmissa funktioissa on muuttuja i silmukassa? Koodi toimii ongelmitta, ja syy on jälleen kerran siinä, että tässä on kyseessä kaksi eri i-nimistä muuttujaa eri
näkyvyysalueilla
. Tämä ihan vain muistutuksena jos funktioiden käytön eduista tämä yksi on päässyt unohtumaan.Tulos näyttää nyt siis halutulta:
1. Alcest, Kodama, 6, 0:42:15, 2016 2. Canaan, A Calling to Weakness, 17, 1:11:17, 2002 3. Deftones, Gore, 11, 0:48:13, 2016 4. Funeralium, Deceived Idealism, 6, 1:28:22, 2013 -- paina enter jatkaaksesi tulostusta -- 5. IU, Modern Times, 13, 47:14, 2013 6. Mono, You Are There, 6, 1:00:01, 2006 7. Panopticon, Roads to the North, 8, 1:11:07, 2014 8. PassCode, Clarity, 13, 49:27, 2019 9. Scandal, Hello World, 13, 53:22, 2014 10. Slipknot, Iowa, 14, 1:06:24, 2001 -- paina enter jatkaaksesi tulostusta -- 11. Wolves in the Throne Room, Thrice Woven, 5, 42:19, 2017
Viimeistely¶
Tulostukset ovat vielä hieman rumia, ja turhat nollatunnit jäävät näkyviin. Muokataan siis lopuksi uutta muotoile_sivu-funktiota. Muutokset kohdistuvat pelkästään merkkijonon muotoiluun, jolle tehdään muutamia editointeja:
def muotoile_sivu(rivit, sivu):
for i, levy in enumerate(rivit, sivu * PER_SIVU + 1):
print(
f"{i:2}. "
f"{levy['artisti']} - {levy['albumi']} ({levy['julkaisuvuosi']}) "
f"[{levy['kpl_n']}] [{levy['kesto'].lstrip('0:')}]"
)
Muotoilun sapluuna on kirjoitettu uusiksi, ja olemme palauttaneet käyttöön
avainsanat
. Keston kohdalla oleva lstrip poistaa nollatunnit kestosta, mikäli niitä löytää - ja itse asiassa myös nollaminuutit jos levy on tarpeeksi lyhyt (strip-metodin toimintaan kannattaa perehtyä tarkasti!). Lopputulos näyttää jo suorastaan kauniilta:1. Alcest - Kodama (2016) [6] [42:15] 2. Canaan - A Calling to Weakness (2002) [17] [1:11:17] 3. Deftones - Gore (2016) [11] [48:13] 4. Funeralium - Deceived Idealism (2013) [6] [1:28:22] 5. IU - Modern Times (2013) [13] [47:14] -- paina enter jatkaaksesi tulostusta -- 6. Mono - You Are There (2006) [6] [1:00:01] 7. Panopticon - Roads to the North (2014) [8] [1:11:07] 8. PassCode - Clarity (2019) [13] [49:27] 9. Scandal - Hello World (2014) [13] [53:22] 10. Slipknot - Iowa (2001) [14] [1:06:24] -- paina enter jatkaaksesi tulostusta -- 11. Wolves in the Throne Room - Thrice Woven (2017) [5] [42:19]
Sen sijaan koodi itsessään ei välttämättä enää ole kauneimmasta päästä, koska muotoilumerkkijonon sisällä alkaa olla ruuhkaisaa. Tässä kohtaa voi harkita, olisiko vanha
format-metodi
parempi vaihtoehto. Metodin tärkein ero muotoilumerkkijonoon
on se, että paikanpitimiin sijoitetut arvot täytyy osoittaa erikseen metodin argumenteilla, siinä missä f-merkkijono poimii nimet automaattisesti ohjelman nimiavaruudesta
. Alla olevassa esimerkissä koodirivejä on kyllä enemmän, mutta itse merkkijono näyttää paljon selkeämmältä.def muotoile_sivu(rivit, sivu):
for i, levy in enumerate(rivit, sivu * PER_SIVU + 1):
print("{i:2}. {artisti} - {albumi} ({vuosi}) [{kpl_n}] [{kesto}]".format(
i=i,
artisti=levy["artisti"],
albumi=levy["albumi"],
kpl_n=levy["kpl_n"],
kesto=levy["kesto"].lstrip("0:"),
vuosi=levy["julkaisuvuosi"]
))
Seuraavassa jaksossa...¶
Ohjelmointrillerin huikeassa päätösluvussa teemme vielä listojen kanssa pari juttua, jotka eivät tähän eepokseen mahtuneet: alkioiden poistaminen ja niiden arvojen muuttaminen. Opimme myös tallentamaan kokoelman ajojen välillä, mikä on varmaan ihan hyvä ominaisuus, jos ohjelmaa ei aio pitää ikuisesti päällä.
Niin ja sellainen pikkudetalji kuin graafisten käyttöliittymien alkeet on myös mahdutettu päätöslukuun...
Loppusanat¶
Silmukat
ja listat
muodostavat työkalupakin, jolla pystyy yhdessä aiemmin opittujen asioiden kanssa tekemään käytännössä mitä tahansa. Erityisesti Pythonin listat ovat todella monikäyttöisiä. Mitä enemmän ohjelmoit Pythonilla, sitä useammin tulet huomaamaan, että lista on ratkaisu kohtaamaasi ongelmaan. Silmukat kulkevat usein listojen kavereina, koska ne ovat ainoa keino käydä järkevästi läpi listojen sisältämiä arvoja
.Myös se, miten paljon etua on ohjelman jakamisesta
funktioihin
, alkoi näkyä tässä materiaalissa aiempaa selkeämmin. Monikäyttöiset kyselyfunktiot helpottavat huomattavasti ydintoiminnan toteutusta kuin myös tulostamisen sijoittaminen omaksi funktiokseen. Myös ohjelman toimintojen jako omiin funktioihinsa on aiempaa vaikuttavampaa, koska ne todella tekevät erilaisia asioita.Ylipäätään ohjelman suunnittelu osoittautui tässä materiaalissa entistä tärkeämmäksi. Huomasimme että nykyinenkin ratkaisu olisi voitu tehdä paremmin. Näin käy usein riippumatta siitä, kuinka kokenut koodari on kyseessä, ja kuinka mones iteraatio samasta ohjelmasta on kyseessä.
Kun työkalupakki monipuolistuu, kaikkein oleellisin ohjelmointitaito on lopulta pystyä työskentelemään järjestelmällisesti. Jos aloittaa koodaamaan yhtä osaa ohjelmasta miettimättä lainkaan muita osia, saattaa upottaa itsensä suohon, josta mikään ei pelasta. Tämän materiaalin tiedoilla saa jo vahvan pohjan oman lopputyön suunnitelmalliseen tekemiseen ja sitä seuraavista harjoituksista vielä lisävauhtia.
Kuvalähteet¶
- 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-SA 2.0
- alkuperäinen lisenssi: CC-BY-NC 2.0 (teksti lisätty)
- alkuperäinen lisenssi: CC-BY-NC-SA 2.0
- alkuperäinen lisenssi: CC-BY 2.0 (teksti lisätty)
- alkuperäinen lisenssi: CC-BY 2.0 (teksti lisätty)
Anna palautetta
Kommentteja materiaalista?