Osa 5

Viittaukset

Olemme tähän asti ajatelleet, että muuttuja on eräänlainen "laatikko", joka sisältää muuttujan arvon. Teknisesti ottaen tämä ei pidä paikkaansa Pythonissa: muuttujat eivät sisällä arvoa vaan ne viittaavat arvona olevaan olioon, kuten lukuun, merkkijonoon tai listaan.

Käytännössä tämä tarkoittaa, että muuttujaan ei tallenneta arvoa, vaan tieto siitä paikasta, mistä muuttujan arvo löytyy.

Viittausta voidaan kuvata nuolena muuttujasta sen varsinaiseen arvoon:

5 2 1

Viittaus siis kertoo, mistä varsinainen arvo löytyy. Funktio id kertoo, mihin muuttuja viittaa:

a = [1, 2, 3]
print(id(a))
b = "Tämäkin on viittaus"
print(id(b))
Esimerkkitulostus

4538357072 4537788912

Viittaus eli muuttujan id on kokonaisluku, jonka voi ajatella olevan muuttujan arvon sijainnin osoite tietokoneen muistissa. Huomaa, että jos suoritat yllä olevan koodin omalla koneellasi, tulos on luultavasti erilainen, koska muuttujilla on eri viitteet.

Kuten jo edellisen osan esimerkistä näimme, Python Tutorin visualisaattori näyttää viitteet "nuolina" varsinaiseen sisältöön. Visualisaattori kuitenkin "huijaa" merkkijonojen tapauksessa ja näyttää ne ikään kuin merkkijonon sisältö olisi tallennettu muuttujan sisälle:

5 2 1a

Näin ei kuitaan ole todellisuudessa, vaan merkkijonotkin käsitellään Pythonin sisäisissä rakenteissa samaan tapaan kuin listat.

Monet Pythonin sisäänrakennetut tyypit, kuten str, ovat muuttumattomia. Tämä tarkoittaa, että olion arvo ei voi koskaan muuttua. Sen sijaan arvo voidaan korvata uudella arvolla:

5 2 2

Pythonissa on myös tyyppejä, jotka ovat muuttuvia. Esimerkiksi listan sisältö voi muuttua ilman, että tarvitsee luoda kokonaan uusi lista:

5 2 3

Hieman yllättäen myös lukuja ja totuusarvoja edustavat perustietotyypit int, float ja bool ovat muuttumattomia. Tarkastellaan esimerkkinä seuraavaa koodia:

luku = 1
luku = 2
luku += 10

Vaikka vaikuttaa siltä, että koodi muuttaa lukua, teknisesti ottaen ei näin ole, vaan jokainen komento luo uuden luvun.

Seuraavan ohjelman tulostus on mielenkiintoinen:

luku = 1
print(id(luku))
luku += 10
print(id(luku))
a = 1
print(id(a))
Esimerkkitulostus

4535856912 4535856944 4535856912

Aluksi muuttuja luku viittaa paikkaan 4535856912, ja kun muuttujan arvo muuttuu, se alkaa viitata paikkaan 4535856944. Kun muuttujaan a sijoitetaan arvo 1, se alkaa viitata samaan paikkaan kuin mihin luku viittasi, kun sen arvo oli 1.

Vaikuttaakin siltä, että Python on tallentanut luvun 1 paikkaan 4535856912 ja aina kun jonkin muuttujan arvona on 1, muuttuja viittaa tuohon paikkaan "tietokoneen muistissa".

Vaikka perustietotyypit int, float ja bool ovat viittauksia, ohjelmoijan ei oikeastaan tarvitse välittää asiasta.

Useampi viittaus samaan listaan

Tarkastellaan esimerkkinä listamuuttujan arvon kopiointia:

a = [1, 2, 3]
b = a
b[0] = 10

Sijoitus b = a kopioi muuttujan a arvon muuttujaan b. On tärkeä kuitenkin huomata, että muuttujan arvona ei ole lista vaan viittaus listaan.

Sijoitus b = a siis kopioi viittauksen, minkä seurauksena kopioinnin jälkeen samaan listaan on kaksi viittausta:

5 2 4

Listaa voidaan käsitellä kumman tahansa viittauksen avulla:

lista = [1, 2, 3, 4]
lista2 = lista

lista[0] = 10
lista2[1] = 20

print(lista)
print(lista2)
Esimerkkitulostus

[10, 20, 3, 4] [10, 20, 3, 4]

Mikäli samaan listaan on useampia viittauksia, sitä voidaan käsitellä minkä tahansa viittauksen kautta samalla tavalla. Toisaalta yhden viittauksen kautta tehtävä muutos heijastuu myös muihin viittauksiin.

Visualisaattori näyttää jälleen selkeästi mitä ohjelmassa tapahtuu:

5 2 4a

Listan kopiointi

Jos haluamme tehdä listasta erillisen kopion, voimme luoda uuden listan ja lisätä siihen jokaisen aluperäisen listan alkion:

lista = [1, 2, 3, 3, 5]

kopio = []
for alkio in lista:
    kopio.append(alkio)

kopio[0] = 10
kopio.append(6)
print("lista", lista)
print("kopio", kopio)
Esimerkkitulostus

lista [1, 2, 3, 3, 5] kopio [10, 2, 3, 3, 5, 6]

Visualisaattorilla tarkastellen kopiointi näyttää seuraavalta:

5 2 4b

Muuttuja kopio siis viittaa nyt eri listaan kuin muuttuja lista.

Helpompi tapa listan kopioimiseen on hyödyntää []-operaattoria, johon tutustuimme aiemmin kurssilla. Merkintä [:] tarkoittaa, että listalta valitaan kaikki alkiot, ja tämän sivuvaikutuksena syntyy kopio listasta:

lista = [1,2,3,4]
kopio = lista[:]

lista[0] = 10
kopio[1] = 20

print(lista)
print(kopio)
Esimerkkitulostus

[10, 2, 3, 4] [1, 20, 3, 4]

Lista funktion parametrina

Kun lista välitetään parametrina funktiolle, välitetään viittaus listaan. Tämä tarkoittaa, että funktio voi muuttaa parametrinaan saamaansa listaa.

Esimerkiksi seuraava funktio lisää uuden alkion parametrinaan saamaansa listaan:

def lisaa_alkio(lista: list):
    uusi_alkio = 10
    lista.append(uusi_alkio)

lista = [1,2,3]
print(lista)
lisaa_alkio(lista)
print(lista)
Esimerkkitulostus
[1, 2, 3] [1, 2, 3, 10]

Huomaa, että funktio lisaa_alkio ei palauta mitään, vaan muuttaa parametrinaan saamaansa listaa.

Visualisaattori havainnollistaa tilanteen seuraavasti:

5 2 4c

Global frame tarkoittaa pääohjelman muuttujia ja sinisellä oleva laatikko lisaa_alkio taas funktion parametreja ja muuttujia. Kuten visualisaatio havainnollistaa, funktio viittaa samaan listaan mihin pääohjelmakin viittaa, eli funktiossa listalle tehtävät muutokset näkyvät pääohjelmaan.

Toinen tapa olisi luoda uusi lista ja palauttaa se:

def lisaa_alkio(lista: list) -> list:
    uusi_alkio = 10
    kopio = lista[:]
    kopio.append(uusi_alkio)
    return kopio

luvut = [1, 2, 3]
luvut2 = lisaa_alkio(luvut)

print("Alkuperäinen lista:", luvut)
print("Uusi lista:", luvut2)
Esimerkkitulostus

Alkuperäinen lista: [1, 2, 3] Uusi lista: [1, 2, 3, 10]

Jos et ole 100% varma mitä koodissa tapahtuu, käy sen toiminta läpi visualisaattorilla!

Parametrina olevan listan muokkaaminen

Seuraavassa on yritys tehdä funktio, joka kasvattaa parametrina saamansa listan jokaista alkiota kymmenellä:

def kasvata_kaikkia(lista: list):
    uusilista = []
    for alkio in lista:
        uusilista.append(alkio + 10)
    lista = uusilista

luvut = [1, 2, 3]
print("alussa ",luvut)
kasvata_kaikkia(luvut)
print("funktion jälkeen", luvut)
Esimerkkitulostus

alussa: [1, 2, 3] funktion jälkeen: [1, 2, 3]

Jostain syystä funktio ei kuitenkaan näytä toimivan. Mistä on kyse?

Funktiolle on välitetty parametrina viite muutettavaan listaan. Sijoitus lista = uusilista saa aikaan sen, että parametriin talletettu viite muuttaa arvoaan funktion sisällä eli se alkaa viitata funktion sisällä luotuun uuteen listaan. Sijoitus ei kuitenkaan vaikuta funktion ulkopuolelle, siellä viitataan edelleen alkuperäiseen listaan.

Seuraava kuvasarja havainnollistaa, mihin eri muuttujat viittaavat ohjelman suorituksen aikana:

5 2 6

Funktion sisällä muutettu lista siis "kadotetaan" kun funktiosta palataan, ja muuttuja luvut viittaa koko ajan alkuperäiseen listaan.

Visualisaattori on tässäkin tapauksessa ystävä: se näyttää selkeästi, miten funktio ei koske alkuperäiseen listaan ollenkaan vaan luo uuden listan, johon muutokset tehdään:

5 2 4d

Yksi tapa korjata ongelma on kopioida uuden listan kaikki alkiot takaisin vanhaan listaan:

def kasvata_kaikkia(lista: list):
    uusilista = []
    for alkio in lista:
        uusilista.append(alkio + 10)

    # kopioidaan vanhaan listaan uuden listan arvot
    for i in range(len(lista)):
        lista[i] = uusilista[i]

Pythonissa on olemassa myös ovela tapa sijoittaa monta alkiota kerrallaan listaan:

>>> lista = [1, 2, 3, 4]
>>> lista[1:3] = [10, 20]
>>> lista
[1, 10, 20, 4]

Esimerkissä siis sijoitetaan "osalistaan" eli listan kohtiin 1 ja 2 taulukollinen alkioita.

Osalistaksi voidaan myös valita koko lista:

>>> lista = [1, 2, 3, 4]
>>> lista[:] = [100, 99, 98, 97]
>>> lista
[100, 99, 98, 97]

Eli näin tulee korvatuksi koko vanhan listan sisältö. Siispä eräs toimiva versio funktiosta näyttää seuraavalta:

def kasvata_kaikkia(lista: list):
    uusilista = []
    for alkio in lista:
        uusilista.append(alkio + 10)

    lista[:] = uusilista

...tai ilman listan kopiontia yksinkertaisesti sijoittamalla uudet arvot heti vanhaan listaan:

def kasvata_kaikkia(lista: list):
    for i in range(len(lista)):
        lista[i] += 10
Loading
Loading
Loading
Loading
Loading
Loading

Funktioiden sivuvaikutukset

Koska funktio saa parametrinaan viittauksen listaan, se voi muuttaa tätä listaa. Jos funktion varsinaisena tarkoituksena ei ole muuttaa listaa, muutokset voivat aiheuttaa ongelmia toisaalla ohjelmassa.

Tarkastellaan esimerkkinä funktiota, jonka tarkoituksena on etsiä listan toiseksi pienin alkio:

def toiseksi_pienin(lista: list) -> int:
    # järjestetyn listan toiseksi pienin alkio on kohdassa 1
    lista.sort()
    return lista[1]

luvut = [1, 4, 2, 5, 3, 6, 4, 7]
print(toiseksi_pienin(luvut))
print(luvut)
Esimerkkitulostus
2 [1, 2, 3, 4, 4, 5, 6, 7]

Funktio kyllä etsii ja löytää toiseksi pienimmän alkion, mutta sen lisäksi se muuttaa listan alkioiden järjestyksen. Jos järjestyksellä on merkitystä muualla ohjelmassa, funktion kutsuminen voi aiheuttaa virheitä. Esimerkin kaltaista muutosta viittauksena saatuun olioon kutsutaan funktion sivuvaikutukseksi.

Voimme toteuttaa funktion ilman sivuvaikutuksia näin:

def toiseksi_pienin(lista: list) -> int:
    kopio = sorted(lista)
    return kopio[1]

luvut = [1, 4, 2, 5, 3, 6, 4, 7]
print(toiseksi_pienin(luvut))
print(luvut)
Esimerkkitulostus

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

Koska funktio sorted palauttaa uuden järjestetyn listan, toiseksi pienimmän alkion etsiminen ei enää sotke listan alkuperäistä järjestystä.

Usein pidetään hyvänä asiana, että funktiot eivät aiheuta sivuvaikutuksia, sillä sivuvaikutukset voivat hankaloittaa ohjelmien toimivuuden varmistamista.

Sivuvaikutuksettomia funktioita kutsutaan myös puhtaiksi funktioiksi ja erityisesti funktionaalista ohjelmointityyliä käytettäessä funktiot pyritään rakentamaan näin. Palaamme aiheeseen tarkemmin Ohjelmoinnin jatkokurssilla.

Loading...
:
Loading...

Log in to view the quiz

Seuraava osa: