Funktionaalista ohjelmointia
Funktionaalisella ohjelmoinnilla tarkoitetaan ohjelmointiparadigmaa, jossa vältetään tilan muutoksia mahdollisimman pitkälle. Muuttujien sijasta ohjelman suoritus perustuu funktionaalisessa ohjelmoinnissa mahdollisimman pitkälti funktioiden keskinäisiin kutsuihin.
Aikaisemmin esitetyt lambda-lausekkeet ja listakoosteet ovat esimerkkejä funktionaalisesta ohjelmointityylistä, koska niitä käyttämällä voidaan välttää ohjelman tilan muutokset - esimerkiksi lambda-lausekkeella voimme luoda funktion ilman että viittausta siihen tallennetaan mihinkään.
Funktionaalinen ohjelmointi on esimerkki ohjelmointiparadigmasta eli ohjelmointityylistä. Muita tyypillisiä ja kurssilla jo aiemmin käsiteltyjä paradigmoja ovat esimerkiksi
- imperatiivinen paradigma, joka perustuu peräkkäisiin komentoihin ja niiden suorittamiseen järjestyksessä
- proseduraalinen paradigma, jossa ohjelma jaetaan pienempiin aliohjelmiin. Imperatiivinen ja proseduraalinen paradigma tarkoittavat joidenkin määrittelyjen mukaan samaa asiaa.
- olio-ohjelmointi, jossa ohjelma ja sen tila mallinnetaan luokista muodostettujen olioiden avulla.
Pythonin monipuolisuus tulee hyvin esille siinä, että voimme hyödyntää siinä useita eri paradigmoja - jopa samoissa ohjelmissa. Näin voimme hyödyntää tehokkainta ja selkeintä tapaa ongelmien ratkaisemiseksi.
Tarkastellaan vielä muutamaa funktionaalisen ohjelmoinnin työkalua Pythonissa.
map
Funktio map
suorittaa annetun operaation kaikille annetun iteroitavan sarjan alkioille. Niinpä map
muistuttaa koostetta monessa mielessä, syntaksi tosin näyttää erilaiselta.
Tarkastellaan esimerkkinä funktiokutsua, joka muuttaa merkkijonot kokonaisluvuiksi:
mjonolista = ["123","-10", "23", "98", "0", "-110"]
luvut = map(lambda x : int(x), mjonolista)
print(luvut)
for luku in luvut:
print(luku)
<map object at 0x0000021A4BFA9A90> 123 -10 23 98 0 -110
Funktion map
yleinen syntaksi on siis
map(<funktio, jota alkioille kutsutaan>, <sarja, jonka alkioille funktiota kutsutaan>)
Funktio palauttaa map-tyyppisen objektin, jonka voi joko iteroida läpi for-lauseella tai esimerkiksi muuttaa listaksi list
-funktiolla:
def alkukirjain_isoksi(mjono: str):
alku = mjono[0]
alku = alku.upper()
return alku + mjono[1:]
testilista = ["eka", "toka", "kolmas", "neljäs"]
valmiit = map(alkukirjain_isoksi, testilista)
valmiit_lista = list(valmiit)
print(valmiit_lista)
['Eka', 'Toka', 'Kolmas', 'Neljäs']
Kuten esimerkistä huomataan, map-funktiossa voi tietysti käyttää lambda-lausekkeella luodun funktion lisäksi myös def
-avainsanalla aiemmin määriteltyä nimettyä funktiota.
Edellinen esimerkki voitaisiin toteuttaa myös vaikkapa listakoosteen avulla, esimerkiksi:
def alkukirjain_isoksi(mjono: str):
alku = mjono[0]
alku = alku.upper()
return alku + mjono[1:]
testilista = ["eka", "toka", "kolmas", "neljäs"]
valmiit_lista = [alkukirjain_isoksi(alkio) for alkio in testilista]
print(valmiit_lista)
...tai esimerkiksi iteroimalla lista läpi for-lauseella ja tallentamalla käsitellyt alkiot uuteen listaan append
-metodilla. Onkin tyypillistä, että saman asian voi toteuttaa usealla eri tavalla. Eri vaihtoehtojen tunteminen auttaa valitsemaan niistä ohjelmaan (ja omaan makuun) parhaiten sopivan.
Kannattaa huomata, että map
-funktion palauttama lopputulos ei ole lista, vaan iteraattori-olio ja vaikka se käyttäytyykin listan tapaan monissa tilanteissa, niin näin ei ole aina.
Tarkastellaan seuraavaa esimerkkiä:
def alkukirjain_isoksi(mjono: str):
alku = mjono[0]
alku = alku.upper()
return alku + mjono[1:]
testilista = ["eka", "toka", "kolmas", "neljäs"]
# talletetaan map-funktion tulos
valmiit = map(alkukirjain_isoksi, testilista)
for sana in valmiit:
print(sana)
print("sama uusiksi:")
for sana in valmiit:
print(sana)
Tulostus on seuraava:
Eka Toka Kolmas Neljäs sama uusiksi:
Eli kun map
-funktion tuloksena olevat nimet yritetään tulostaa toiseen kertaan, ei tulostu mitään. Syynä tälle on se, läpikäynti for
-lauseella käy iteraattorin oliot jo läpi, ja kun samaa yritetään toistamiseen, ei ole enää mitään läpikäytävää!
Jos ohjelma haluaa tarkastella map
-funktion tulosta useampaan kertaan, tulee tulos esimerkiksi muuttaa listaksi antamalla se parametriksi list
-konstruktorille:
testilista = ["eka", "toka", "kolmas", "neljäs"]
# muutetaan map-funktion palauttama iteraattori listaksi
valmiit = list(map(alkukirjain_isoksi, testilista))
for sana in valmiit:
print(sana)
print("sama uusiksi:")
for sana in valmiit:
print(sana)
Eka Toka Kolmas Neljäs sama uusiksi: Eka Toka Kolmas Neljäs
map ja oliot
Funktiolla map
voidaan toki käsitellä myös omien luokkien olioita. Asiaan ei liity mitään tavanomaisesta poikkeavaa. Tarkastellaan seuraavaa esimerkkiä
class Pankkitili:
def __init__(self, numero: str, nimi: str, saldo: float):
self.__numero = numero
self.nimi = nimi
self.__saldo = saldo
def lisaa_rahaa(self, rahasumma: float):
if rahasumma > 0:
self.__saldo += rahasumma
def hae_saldo(self):
return self.__saldo
t1 = Pankkitili("123456", "Reijo Rahakas", 5000)
t2 = Pankkitili("12321", "Keijo Köyhä ", 1)
t3 = Pankkitili("223344", "Maija Miljonääri ", 1000000)
tilit = [t1, t2, t3]
asiakkaat = map(lambda t: t.nimi, tilit)
for nimi in asiakkaat:
print(nimi)
saldot = map(lambda t: t.hae_saldo(), tilit)
for saldo in saldot:
print(saldo)
Reijo Rahakas Keijo Köyhä Maija Miljonääri 5000 1 1000000
Koodissa selvitetään ensin funktion map
avulla tilien omistajat. Huomaa miten lambda-funktiolla haetaan attribuuttina oleva nimi pankkitiliolioista:
asiakkaat = map(lambda t: t.nimi, tilit)
Tämän jälkeen haetaan samalla tyylillä jokaisen pankkitilin saldo. Lambda-funktio on nyt hieman erilainen, sillä saldo saadaan selville kutsumalla pankkitiliolion metodia:
saldot = map(lambda t: t.hae_saldo(), tilit)
filter
Funktio filter
muistuttaa funktiota map
, mutta nimensä mukaisesti se ei poimi kaikkia alkioita lähteestä, vaan ainoastaan ne, joille annettu funktio palauttaa arvon True.
Tarkastellaan taas ensin esimerkkiä funktion käytöstä:
luvut = [1, 2, 3, 5, 6, 4, 9, 10, 14, 15]
parilliset = filter(lambda luku: luku % 2 == 0, luvut)
for luku in parilliset:
print(luku)
2 6 4 10 14
Sama esimerkki voitaisiin kirjoittaa ilman lambda-lauseketta määrittelemällä funktio def
-avainsanalla:
def onko_parillinen(luku: int):
if luku % 2 == 0:
return True
return False
luvut = [1, 2, 3, 5, 6, 4, 9, 10, 14, 15]
parilliset = filter(onko_parillinen, luvut)
for luku in parilliset:
print(luku)
Toiminnallisuuden kannalta ohjelmat ovat täysin yhtäläiset. Onkin mielipidekysymys kumpaa pitää selkeämpänä.
Tarkastellaan vielä toista esimerkkiä suodattamisesta. Ohjelmassa poimitaan kalalistasta ainoastaan ne kalat, jotka ovat vähintään 1000 gramman painoisia:
class Kala:
""" Luokka mallintaa tietynpainoista kalaa """
def __init__(self, laji: str, paino: int):
self.laji = laji
self.paino = paino
def __repr__(self):
return f"{self.laji} ({self.paino} g.)"
if __name__ == "__main__":
k1 = Kala("Hauki", 1870)
k2 = Kala("Ahven", 763)
k3 = Kala("Hauki", 3410)
k4 = Kala("Turska", 2449)
k5 = Kala("Särki", 210)
kalat = [k1, k2, k3, k4, k5]
ylikiloiset = filter(lambda kala : kala.paino >= 1000, kalat)
for kala in ylikiloiset:
print(kala)
Hauki (1870 g.) Hauki (3410 g.) Turska (2449 g.)
Taas kerran sama voitaisiin toteuttaa listakoosteena:
ylikiloiset = [kala for kala in kalat if kala.paino >= 1000]
filter palauttaa iteraattorin
Funktion map
tapaan, myös funktio filter
palauttaa listan sijaan iteraattorin ja on tilanteita joissa on syytä olla varuillaan sillä iteraattorin voi käydä läpi vain kerran. Eli seuraava yritys tulostaa suuret kalat kahteen kertaan ei onnistu:
k1 = Kala("Hauki", 1870)
k2 = Kala("Ahven", 763)
k3 = Kala("Hauki", 3410)
k4 = Kala("Turska", 2449)
k5 = Kala("Särki", 210)
kalat = [k1, k2, k3, k4, k5]
ylikiloiset = filter(lambda kala : kala.paino >= 1000, kalat)
for kala in ylikiloiset:
print(kala)
print("sama uudelleen")
for kala in ylikiloiset:
print(kala)
Tulostuu
Hauki (1870 g.) Hauki (3410 g.) Turska (2449 g.) sama uudelleen
Jos funktion filter
tulosta on tarve käsitellä useaan kertaan, tulee se muuttaa esimerkiksi listaksi:
kalat = [k1, k2, k3, k4, k5]
# muutetaan tulos listaksi kutsumalla list-konstruktorioa
ylikiloiset = list(filter(lambda kala : kala.paino >= 1000, kalat))
reduce
Viimeinen tarkastelemamme funktio on reduce
. Kuten funktion nimi vihjaa, sen tarkoituksena on vähentää sarjan alkioiden määrä. Itse asiassa alkioiden sijasta reduce
palauttaa yksittäisen arvon.
Reduce toimii sitten, että se pitää mukanaan koko ajan arvoa, jota se muuttaa yksi kerrallaan käydessään läpi listan alkioita.
Seuraavassa on esimerkki, joka summaa reduce
-funktion avulla listan luvut yhteen. Huomaa, että Pythonin versiosta 3 alkaen funktio reduce
pitää erikseen ottaa käyttöön moduulista functools
.
from functools import reduce
lista = [2, 3, 1, 5]
lukujen_summa = reduce(lambda summa, alkio: summa + alkio, lista, 0)
print(lukujen_summa)
11
Tarkastellaan esimerkkiä hieman tarkemmin. Funktio reduce
saa kolme parametria. Parametreista toisena on läpikäytävä lista, ja kolmantena on laskennan alkuarvo. Koska laskemme listan alkioiden summaa, on sopiva alkuarvo nolla.
Ensimmäisenä parametrina on funktio, joka suorittaa toimenpiteen yksi kerrallaan kullekin listan alkiolle. Tällä kertaa funktio on seuraava:
lambda summa, alkio: summa + alkio
Funktiolla on kaksi parametria. Näistä ensimmäinen on laskennan sen hetkinen tulos ja toinen parametri on käsittelyvuorossa oleva listan alkio. Funktio laskee uuden arvon parametriensa perusteella. Tässä tapauksessa uusi arvio on vanha summa plus kyseisen alkion arvo.
Funktion reduce
toiminta hahmottuu kenties selkeämmin, jos käytetään lambdan sijaan normaalia funktiota apuna ja tehdään funktiosta aputulostuksia:
from functools import reduce
lista = [2, 3, 1, 5]
# reducen apufunktio joka huolehtii yhden alkion arvon lisäämisestä summaan
def summaaja(summa, alkio):
print(f"summa nyt {summa}, vuorossa alkio {alkio}")
# uusi summa on vanha summa + alkion arvo
return summa + alkio
lukujen_summa = reduce(summaaja, lista, 0)
print(lukujen_summa)
Ohjelma tulostaa:
summa nyt 0, vuorossa alkio 2 summa nyt 2, vuorossa alkio 3 summa nyt 5, vuorossa alkio 1 summa nyt 6, vuorossa alkio 5 11
Ensimmäisenä siis käsitellään listan alkio, jonka arvo on 2. Tässä vaiheessa summa on 0, eli sillä on reducelle annettu alkuarvo. Funktio laskee ja palauttaa näiden summan eli 0 + 2.
Tämä arvo on parametrin summa
arvona kun funktiota kutsutaan seuraavalle listan alkiolle eli luvulle 3. Funktio laskee ja palauttaa 2 + 3, joka taas toimii parametrina seuraavalle funktiokutsulle.
Toinen esimerkkimme laskee kaikkien listassa olevien kokonaislukujen tulon.
from functools import reduce
lista = [2, 2, 4, 3, 5, 2]
tulo = reduce(lambda tulo, alkio: tulo * alkio, lista, 1)
print(tulo)
480
Koska on kyse tulosta, ei alkuarvo voi olla nyt 0 (miten käy jos se olisi nolla?), vaan sopiva arvo sille on 1.
Aivan kuten filter
ja map
, myös reduce
voi käsitellä minkä tahansa tyyppisiä olioita.
Tarkastellaan esimerkkinä pankin tilien yhteenlasketun saldon selvittämistä reducella:
class Pankkitili:
def __init__(self, numero: str, nimi: str, saldo: float):
self.__numero = numero
self.nimi = nimi
self.__saldo = saldo
def lisaa_rahaa(self, rahasumma: float):
if rahasumma > 0:
self.__saldo += rahasumma
def hae_saldo(self):
return self.__saldo
t1 = Pankkitili("123456", "Reijo Rahakas", 5000)
t2 = Pankkitili("12321", "Keijo Köyhä ", 1)
t3 = Pankkitili("223344", "Maija Miljonääri ", 1000000)
tilit = [t1, t2, t3]
from functools import reduce
def saldojen_summaaja(yht_saldo, tili):
return yht_saldo + tili.hae_saldo()
saldot_yhteensa = reduce(saldojen_summaaja, tilit, 0)
print("pankissa rahaa yhteensä")
print(saldot_yhteensa)
Ohjelma tulostaa:
pankissa rahaa yhteensä 1005001
Huomaa miten funktio saldojen_summaaja
"kaivaa" saldon jokaisen tiliolion sisältä kutsumalla tilille saldon palauttavaa metodia:
def saldojen_summaaja(yht_saldo, tili):
return yht_saldo + tili.hae_saldo()