Osa 5

Lisää listoista

Lisää listoista

Viime kerralla käsiteltiin lähes yksinomaan listoja, joissa alkiot ovat kokonaislukuja. Listoihin voi kuitenkin tallentaa minkä tahansa tyyppisiä arvoja. Esimerkiksi voimme tallentaa listaan merkkijonoja:

nimet = ["Maija", "Liisa", "Pekka"]
print(nimet)
nimet.append("Kalle")
print(nimet)

print("Listalla nimiä:", len(nimet))
print("Nimet aakkosjärjestyksessä:")
nimet.sort()
for nimi in nimet:
  print(nimi)
Esimerkkitulostus

['Maija', 'Liisa', 'Pekka'] ['Maija', 'Liisa', 'Pekka', 'Kalle'] Listalla nimiä: 4 Nimet aakkosjärjestyksessä: Kalle Liisa Maija Pekka

Samalla tavalla listalle voidaan tallentaa liukulukuja:

mittaukset = [-2.5, 1.1, 7.5, 14.6, 21.0, 19.2]

for mittaus in mittaukset:
    print(mittaus)

keskiarvo = sum(mittaukset) / len(mittaukset)

print("Keskiarvo:", keskiarvo)
Esimerkkitulostus

-2.5 1.1 7.5 14.6 21.0 19.2 Keskiarvo: 10.15

Muistutus: globaalin muuttujan käytön sudenkuoppa

Kuten olemme nähneet, funktioiden sisällä on mahdollista määritellä muuttujia. Kannattaa myös huomata se, että funktio näkee sen ulkopuolella, eli pääohjelmassa määritellyt muuttujat. Tälläisia muuttujia sanotaan globaaleiksi muuttujiksi.

Globalien muuttujien käyttämistä funktioista käsin ei useimmiten pidetä hyvänä asiana muun muassa siksi, että ne saattavat johtaa ikäviin bugeihin.

Seuraavassa on esimerkki funktiosta, joka käyttää "vahingossa" globaalia muuttujaa:

def tulosta_vaarinpain(nimet: list):
    # käytetään vahingossa parametrin sijaan globaalia muuttujaa nimilista
    i = len(nimilista) - 1
    while i>=0:
        print(nimilista[i])
        i -= 1

# globaali muuttuja
nimilista = ["Antti", "Emilia", "Erkki", "Margaret"]
tulosta_vaarinpain(nimilista)
print()
tulosta_vaarinpain(["Tupu", "Hupu", "Lupu"])
Esimerkkitulostus

Margaret Erkki Emilia Antti

Margaret Erkki Emilia Antti

Vaikka funktiota kutsutaan oikein, se tulostaa aina globaalissa muuttujassa nimilista olevat nimet.

Kuten olemme nähneet, kaikki funktioita testaava koodi on kirjoitettava erillisen lohkon sisälle, jotta TMC-testit hyväksyisivät koodin. Edellinen esimerkki siis tulisi toteuttaa seuraavasti:

def tulosta_vaarinpain(nimet: list):
    # käytetään vahingossa parametrin sijaan globaalia muuttujaa nimilista
    i = len(nimilista) - 1
    while i>=0:
        print(nimilista[i])
        i -= 1

# kaikki funktiota testaava koodi tämän lohkon sisälle
if __name__ == "__main__":
    # globaali muuttuja
    nimilista = ["Antti", "Emilia", "Erkki", "Margaret"]
    tulosta_vaarinpain(nimilista)
    print()
    tulosta_vaarinpain(["Tupu", "Hupu", "Lupu"])

Nyt myös globaalin muuttujan määrittely on siirtynyt if-lohkoon.

TMC-testit suoritetaan aina siten, että mitään if-lohkon sisällä olevaa koodia ei huomioida. Tämän takia funktio ei voi edes teoriassa toimia, sillä se viittaa muuttujaan nimilista mitä ei testejä suoritettaessa ole ollenkaan olemassa.

Varoitus: parametrin ylikirjoittaminen ja liian aikainen return

Ennen tämän osan tehtäviin menemistä on syytä kiinnittää huomiota pariin potentiaaliseen ongelmalähteeseen. Tarkastellaan funktiota, joka kertoo löytyykö parametrina oleva luku listalta:

def luku_listalla(luvut: list, luku: int):
    for luku in luvut:
        if luku == luku:
            return True
        else:
            return False

Funktio palauttaa jostain syystä aina True. Syynä tälle on se, että for-silmukka ylikirjoittaa parametrin luku arvon, ja tämän takia if-lauseen ehto on aina tosi.

Ongelmasta päästään eroon nimeämällä parametri uudelleen:

def luku_listalla(luvut: list, etsittava_luku: int):
    for luku in luvut:
        if luku == etsittava_luku:
            return True
        else:
            return False

Nyt if-lauseen ehto on kunnossa. Funktiossa on kuitenkin uusi ongelma, se ei näytä edelleenkään toimivan. Esim. seuraava kokeilu tuo esiin bugin:

on = luku_listalla([1, 2, 3, 4], 3)
print(on)  # tulostuu False

Vika on nyt siinä että funktiosta poistutaan liian aikaisin. Funktio tarkistaa ainoastaan ensimmäisen luvun ja riippuen sen arvosta palauttaa heti joko arvon True tai False. Lopullista tuomiota, eli tietoa siitä että luku ei ole listalla ei voi kuitenkaan antaa ennen kuin kaikki luvut on tarkastettu. Komento return False pitääkin siirtää silmukan ulkopuolelle:

def luku_listalla(luvut: list, etsittava_luku: int):
    for luku in luvut:
        if luku == etsittava_luku:
            return True

    return False

Tarkastellaan vielä yhtä virheellistä esimerkkiä:

def luvut_erisuuret(luvut: list):
    # apumuuttuja, johon kerätään ne luvut jotka on jo tarkastettu
    luvut = []
    for luku in luvut:
        # joko luku on nähty?
        if luku in luvut:
            return False
        luvut.append(luku)

    return True

on = luvut_erisuuret([1, 2, 2])
print(on)  # tulostuu True

Funktio siis yrittää testata ovatko kaikki listan alkiot erisuuria. Se kuitenkin palauttaa aina arvon True.

Ongelmana on jälleen se, että funktio vahingossa ylikirjottaa parametrinsa arvon. Funktio yrittää käyttää muuttujaa luvut pitämään kirjaa jo vastaan tulleista luvuista ja tämä ylikirjoittaa parametrin. Lääke ongelmaan on muuttujan uudelleennimeäminen:

def luvut_erisuuret(luvut: list):
    # apumuuttuja, johon kerätään ne luvut jotka on jo tarkastettu
    havaitut_luvut = []
    for luku in luvut:
        # joko luku on nähty?
        if luku in havaitut_luvut:
            return False
        havaitut_luvut.append(luku)

    return True

on = luvut_erisuuret([1, 2, 2])
print(on)  # tulostuu False

Nämä kuten oikeastaan kaikki koodia vaivaavat ongelmat selviävät debuggerilla tai visualisaattorilla, jonka käytön tärkeyttä ei voi olla korostamatta liikaa.

Loading

Sisäkkäiset listat

Listan alkiot voivat olla myös listoja:

lista = [[5, 2, 3], [4, 1], [2, 2, 5, 1]]
print(lista)
print(lista[1])
print(lista[1][0])
Esimerkkitulostus

[[5, 2, 3], [4, 1], [2, 2, 5, 1]] [4, 1] 4

Mihin voimme käyttää listoja jonka sisällä on listoja?

Voisimme esimerkiksi esittää henkilön tiedot listana, jossa ensimmäisenä alkiona on henkilön nimi, toisena ikä ja kolmantena kengännumero:

["Anu", 10, 26]

Vastaavasti joukko henkilöitä on lista, joka sisältää yksittäisiä henkilöä kuvaavia listoja:

henkilot = [["Anu", 10, 26], ["Petteri", 7, 22], ["Emilia", 32, 37], ["Antti", 39, 44]]

for henkilo in henkilot:
  nimi = henkilo[0]
  ika = henkilo[1]
  kenka = henkilo[2]
  print(f"{nimi}: ikä {ika} vuotta, kengännumero {kenka}")
Esimerkkitulostus

Anu: ikä 10 vuotta, kengännumero 26 Petteri: ikä 7 vuotta, kengännumero 22 Emilia: ikä 32 vuotta, kengännumero 37 Antti: ikä 39 vuotta, kengännumero 44

Huomaa, miten for-lause käy läpi henkilöt yksitellen, eli toiston lohko-osassa muuttuja henkilo saa yksi kerrallaan arvokseen kutakin henkilöä esittävän listan.

Lista ei ole välttämättä paras Pythonin tietorakenne henkilön tietojen esittämiseen. Tutustumme pian sanakirjaan, joka on usein luontevampi tapa hoitaa vastaava tilanne.

Matriisit

Sisäkkäisten listojen avulla voidaan myös esittää matriisi eli kaksiulotteinen taulukko.

Esimerkiksi matriisi

5 1 0

voitaisiin mallintaa kaksiulotteisena listana näin:

matriisi = [[1, 2, 3], [3, 2, 1], [4, 5, 6]]

Koska matriisi on lista listoja, matriisin alkioihin viitataan käyttämällä peräkkäisiä hakasulkuja. Ensimmäinen indeksi viittaa riviin ja toinen sarakkeeseen. Niinpä esimerkiksi m[0][1] tarkoittaa ensimmäisen rivin toista alkiota (kun muistetaan, että indeksointi alkaa nollasta).

matriisi = [[1, 2, 3], [3, 2, 1], [4, 5, 6]]

print(matriisi[0][1])
matriisi[1][0] = 10
print(matriisi)
Esimerkkitulostus

2 [[1, 2, 3], [10, 2, 1], [4, 5, 6]]

Voimme käydä läpi matriisin rivit for-silmukalla. Esimerkiksi seuraava koodi tulostaa matriisin rivit allekkain:

matriisi = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

for rivi in matriisi:
    print(rivi)
Esimerkkitulostus

[1, 2, 3] [4, 5, 6] [7, 8, 9]

Seuraava koodi puolestaan tulostaa matriisin alkiot yksitellen kahden for-silmukan avulla:

matriisi = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

for rivi in matriisi:
    print("uusi rivi")
    for alkio in rivi:
        print(alkio)
Esimerkkitulostus

uusi rivi 1 2 3 uusi rivi 4 5 6 uusi rivi 7 8 9

Sisäkkäisiä listoja käyttävän koodin visualisointi

Jos sisäkkäisiä listoja käsittelevät ohjelmat tuntuvat hankalalta ymmärtää, kannattaa ehdottomasti havainnollistaa niitä Python Tutorin visualisaattorilla. Seuraavassa kuva edellisen esimerkin visualisoinnista:

5 1 0a

Kuten kuva paljastaa, 3x3-matriisi koostuu teknisesti ottaen neljästä listasta. Ensimmäinen lista edustaa koko matriisia ja sen alkioina on erillisiä rivejä edustavat listat.

Kuva havainnollistaa jo sitä seikkaa, josta puhumme tarkemmin seuraavassa osassa: moniulotteisessa listassa listat eivät ole todellisuudessa sisäkkäin, vaan matriisia edustava lista "viittaa" jokaista riviä edustavaan listaan.

Kuvassa tulostus on edennyt matriisin toiselle riville, johon muuttuja rivi parhaillaan viittaa. Muuttuja alkio kertoo sen alkion, jonka kohdalla tulostus on menossa. Muuttujan arvo on nyt keskimmäisen rivin keskimmäinen eli 5.

Lisää matriisin käsittelyä

Matriisin yksittäisten rivien käsittely on helppoa, riittää että valitaan haluttu rivi. Esimerkiksi seuraava funktio laskee halutun rivin alkioiden summan:

def rivin_alkioiden_summa(matriisi, rivi_nro: int):
    # tarkasteluun valitaan yksi rivi
    rivi = matriisi[rivi_nro]
    summa = 0
    for alkio in rivi:
        summa += alkio

    return summa

m = [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]]

summa = rivin_alkioiden_summa(m, 1)
print(summa) # tulostuu 33 (saadaan laskemalla 9 + 1 + 12 + 11)

Jos taas haluttaisiin laskea tietyn sarakkeen eli "pystyrivin" alkioiden summa, tilanne olisi jo monimutkaisempi:

def sarakkeen_alkioiden_summa(matriisi, sarake_nro: int):
    # summaan lisätään kaikkien rivien halutussa kohdassa oleva alkio
    summa = 0
    for rivi in matriisi:
        summa += rivi[sarake_nro]

    return summa

m = [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]]

summa = sarakkeen_alkioiden_summa(m, 2)
print(summa) # tulostuu 39 (saadaan laskemalla 3 + 12 + 9 + 15)

Tarkasteltava sarake siis koostuu jokaisen rivin paikassa 2 olevasta alkiosta.

Näidenkin ohjelmien toiminta kannattaa ehdottomasti käydä läpi visualisaattorilla!

Matriisissa olevan yksittäisen arvon vaihtaminen on helppoa. Riittää että valitaan matriisin sisältä oikea rivi ja sen sisältä sarake:

def vaihda_arvoon(matriisi, rivi_nro: int, sarake_nro: int, arvo: int):
    # haetaan oikea rivi
    rivi = matriisi[rivi_nro]
    # ja sen sisältä oikea kohta
    rivi[sarake_nro] = arvo

m = [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]]

print(m)
vaihda_arvoon(m, 2, 3, 1000)
print(m)
Esimerkkitulostus

[[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 5], [2, 9, 15, 1]] [[4, 2, 3, 2], [9, 1, 12, 11], [7, 8, 9, 1000], [2, 9, 15, 1]]

Mikäli halutaan muuttaa matriisin sisältöä silmukan sisällä, ei ole mahdollista käyttää "normaalia" for-silmukkaa, sillä muutettaessa sisältöä on pakko tietää muutettavien alkioiden indeksit.

Tämä taas onnistuu while-silmukalla tai for-silmukalla hyödyntämällä range-funktiota iteroinnissa. Esimerkiksi seuraava koodi kasvattaa jokaista matriisin alkiota yhdellä:

m = [[1,2,3], [4,5,6], [7,8,9]]

for i in range(len(m)):
    for j in range(len(m[i])):
        m[i][j] += 1

print(m)
Esimerkkitulostus

[[2, 3, 4], [5, 6, 7], [8, 9, 10]]

Ulompi silmukka käy range-funktion avulla läpi arvot nollasta matriisin pituuteen (eli matriisin rivien määrään) ja sisempi silmukka jokaisen rivin alkiot nollasta rivin pituuteen.

Loading

Kaksiulotteinen taulukko pelin tietorakenteena

Matriisi sopii hyvin monien pelien tietorakenteeksi. Esim. sudokun ruudukko

5 1 1

voitaisiin esittää seuraavana matriisina:

sudoku = [
  [9, 0, 0, 0, 8, 0, 3, 0, 0],
  [0, 0, 0, 2, 5, 0, 7, 0, 0],
  [0, 2, 0, 3, 0, 0, 0, 0, 4],
  [0, 9, 4, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 7, 3, 0, 5, 6, 0],
  [7, 0, 5, 0, 6, 0, 4, 0, 0],
  [0, 0, 7, 8, 0, 3, 9, 0, 0],
  [0, 0, 1, 0, 0, 0, 0, 0, 3],
  [3, 0, 0, 0, 0, 0, 0, 0, 2]
]

Arvolla nolla siis kuvataan tilanne, jossa ruutu on vielä tyhjä.

Seuraavassa vielä yksinkertainen versio sudokun tulostavasta metodista:

def tulosta(sudoku):
    for rivi in sudoku:
        for ruutu in rivi:
            if ruutu > 0:
                print(f" {ruutu}", end="")
            else:
                print(" _", end="")
        print()

tulosta(sudoku)

Tulostus näyttää seuraavalta:


 9 _ _ _ 8 _ 3 _ _
 _ _ _ 2 5 _ 7 _ _
 _ 2 _ 3 _ _ _ _ 4
 _ 9 4 _ _ _ _ _ _
 _ _ _ 7 3 _ 5 6 _
 7 _ 5 _ 6 _ 4 _ _
 _ _ 7 8 _ 3 9 _ _
 _ _ 1 _ _ _ _ _ 3
 3 _ _ _ _ _ _ _ 2

Vastaavalla tavalla on mahdollista kuvata moni tuttu peli (esim. shakki, miinaharava, laivan upotus, mastermind, ...) matriisina. Pelistä riippuu, mikä on sopiva tapa "koodata" pelin tilanne matriisiin.

Loading
Loading
Loading
Loading
Loading
Loading...
:
Loading...

Log in to view the quiz

Seuraava osa: