Osa 12

Funktio parametrina

Olemme jo aikaisemmin käyttäneet metodia sort ja funktiota sorted järjestämään listoja luonnolliseen järjestykseen. Metodit toimivat sellaisenaan hyvin luvuista ja merkkijonoista koostuvien listojen kanssa, mutta jos lista sisältää monimutkaisempia alkioita, Python ei välttämättä järjestä listaa niin kuin ohjelmoija toivoisi.

Esimerkiksi lista tupleja järjestetään oletuksena jokaisen tuplen ensimmäisen alkion perusteella:

tuotteet = [("banaani", 5.95), ("omena", 3.95), ("appelsiini", 4.50), ("vesimeloni", 4.95)]

tuotteet.sort()

for tuote in tuotteet:
    print(tuote)
Esimerkkitulostus

('appelsiini', 4.5) ('banaani', 5.95) ('omena', 3.95) ('vesimeloni', 4.95)

Mitä jos haluaisimme järjestää tuotelistan hinnan perusteella?

Funktiot parametrina

Järjestysmetodille tai -funktiolle voidaan antaa toisena parametrina järjestyksen määräävä avain. Avaimeksi annetaan funktio, joka kertoo, miten yksittäisen alkion arvo määritetään. Python kutsuu tätä funktiota järjestämisen aikana alkioiden vertailemiseen.

Esimerkiksi:

def hintajarjestys(alkio: tuple):
    # Palautetaan tuplen toinen alkio eli hinta
    return alkio[1]

if __name__ == "__main__":
    tuotteet = [("banaani", 5.95), ("omena", 3.95), ("appelsiini", 4.50), ("vesimeloni", 4.95)]

    # Hyödynnetään funktiota hintajarjestys
    tuotteet.sort(key=hintajarjestys)

    for tuote in tuotteet:
        print(tuote)
Esimerkkitulostus

('omena', 3.95) ('appelsiini', 4.5) ('vesimeloni', 4.95) ('banaani', 5.95)

Nyt ohjelma järjestää listan hinnan mukaiseen järjestykseen. Mutta mitä ohjelmassa oikeastaan tapahtuu?

Funktion hintajarjestys määrittely on melko yksinkertainen: se saa parametrikseen yhden alkion ja palauttaa alkiolle arvon - tässä tapauksessa tuplen toisen alkion (joka esimerkissämme esittää tuotteen hintaa). Tarkastellaan kuitenkin lähemmin järjestysmetodia kutsuvaa riviä:

tuotteet.sort(key=hintajarjestys)

Rivillä annetaan metodille sort parametriksi funktio. Ei siis funktion paluuarvoa, vaan viittaus funktioon. Järjestysmetodi kutsuu tätä funktiota jokaiselle alkiolle.

Kutsut nähdään selkeästi lisäämällä vertailufunktioomme ylimääräinen tulostuslause:

def hintajarjestys(alkio: tuple):
    # Tulostetaan alkio
    print(f"Kutsuttiin hintajarjestys({alkio})")

    # Palautetaan tuplen toinen alkio eli hinta
    return alkio[1]


tuotteet = [("banaani", 5.95), ("omena", 3.95), ("appelsiini", 4.50), ("vesimeloni", 4.95)]

# Hyödynnetään funktiota hintajarjestys
tuotteet.sort(key=hintajarjestys)

for tuote in tuotteet:
    print(tuote)
Esimerkkitulostus

Kutsuttiin hintajarjestys(('banaani', 5.95)) Kutsuttiin hintajarjestys(('omena', 3.95)) Kutsuttiin hintajarjestys(('appelsiini', 4.5)) Kutsuttiin hintajarjestys(('vesimeloni', 4.95)) ('omena', 3.95) ('appelsiini', 4.5) ('vesimeloni', 4.95) ('banaani', 5.95)

Järjestys saadaan käännettyä päinvastaiseksi hyödyntämällä sekä metodista sort että funktiosta sorted löytyvää toista parametria reverse:

tuotteet.sort(key=hintajarjestys, reverse=True)

t2 = sorted(tuotteet, key=hintajarjestys, reverse=True)

Funktion sisällä määritelty funktio

Jos haluaisimme siirtää edellisessä esimerkissä tehdyn järjestämisen omaan funktioonsa jarjesta_hinnan_mukaan, voisimme toteuttaa sen seuraavasti:

def hintajarjestys(alkio: tuple):
    return alkio[1]

def jarjesta_hinnan_mukaan(alkiot: list):
    # käytetään täällä funktiota hintajarjestys
    return sorted(alkiot, key=hintajarjestys)

tuotteet = [("banaani", 5.95), ("omena", 3.95), ("appelsiini", 4.50), ("vesimeloni", 4.95)]

for tuote in jarjesta_hinnan_mukaan(tuotteet):
    print(tuote)

Jos järjestämisen käyttämää apufunktiota hintajarjestys ei käytetä missään muussa kohtaa ohjelmaa kuin funktiossa jarjesta_hinnan_mukaan, sen määrittely voitaisiin siirtää funktion sisälle:

def jarjesta_hinnan_mukaan(alkiot: list):
    # määritellään apufunktio tällä kertaa funktion sisällä
    def hintajarjestys(alkio: tuple):
        return alkio[1]

    return sorted(alkiot, key=hintajarjestys)
Loading
Loading
Loading

Omien olioiden alkioiden järjestäminen

Kirjoitetaan samaa periaatetta hyödyntäen ohjelma, joka järjestää listan omasta Opiskelija-luokasta luotuja olioita kahden eri kriteerin avulla:

class Opiskelija:
    """ Luokka mallintaa yhtä opiskelijaa """
    def __init__(self, nimi: str, tunnus: str, pisteet: int):
        self.nimi = nimi
        self.tunnus = tunnus
        self.pisteet = pisteet

    def __str__(self):
        return f"{self.nimi} ({self.tunnus}), {self.pisteet} op."


def tunnuksen_mukaan(alkio: Opiskelija):
    return alkio.tunnus

def pisteiden_mukaan(alkio: Opiskelija):
    return alkio.pisteet


if __name__ == "__main__":
    o1 = Opiskelija("Aapeli", "a123", 220)
    o2 = Opiskelija("Maija", "m321", 210)
    o3 = Opiskelija("Anna", "a999", 131)

    opiskelijat = [o1, o2, o3]

    print("Tunnuksen mukaan:")
    for opiskelija in sorted(opiskelijat, key=tunnuksen_mukaan):
        print(opiskelija)

    print()

    print("Pisteiden mukaan:")
    for opiskelija in sorted(opiskelijat, key=pisteiden_mukaan):
        print(opiskelija)
Esimerkkitulostus

Aapeli (a123), 220 op. Anna (a999), 131 op. Maija (m321), 210 op.

Pisteiden mukaan: Anna (a999), 131 op. Maija (m321), 210 op. Aapeli (a123), 220 op

Järjestäminen toimii niinkuin pitää. Jos olioille arvon antavia funktioita tunnuksen_mukaan ja pisteiden_mukaan ei tarvita muuten, voimme kuitenkin vielä yksinkertaistaa ohjelmaa.

Loading
Loading

Lambda-lauseke

Lambda-lausekkeen avulla voidaan luoda anonyymi funktio eli funktio, joka muodostetaan sillä hetkellä, kun sitä tarvitaan. Lausekkeen yleinen syntaksi on seuraava:

lambda <parametrit> : <lauseke>

Esimerkiksi tuplelistan järjestys onnistuisi näin käyttämällä lambda-lauseketta:

tuotteet = [("banaani", 5.95), ("omena", 3.95), ("appelsiini", 4.50), ("vesimeloni", 4.95)]

# Funktio luodaan "lennosta" lambda-lausekkeella:
tuotteet.sort(key=lambda alkio: alkio[1])

for tuote in tuotteet:
    print(tuote)
Esimerkkitulostus

('omena', 3.95) ('appelsiini', 4.5) ('vesimeloni', 4.95) ('banaani', 5.95)

Lauseke

lambda alkio: alkio[1]

vastaa funktiomäärittelyä


def hinta(alkio):
    return alkio[1]

paitsi että lambda-lauseketta käytettäessä funktiolle ei anneta nimeä. Tämän takia muodostettavaa funktiota kutsutaan anonyymiksi funktioksi.

Muuten lambdan avulla muodostettava funktio on kuin mikä tahansa muukin funktio. Esimerkiksi seuraava esimerkki järjestää merkkijonot niiden viimeisten merkkien mukaiseen aakkosjärjestykseen:

mjonot = ["Mikko", "Makke", "Maija", "Markku", "Mikki"]

for jono in sorted(mjonot, key=lambda jono: jono[-1]):
    print(jono)
Esimerkkitulostus

Maija Makke Mikki Mikko Markku

Mennään vielä pidemmälle: yhdistämällä listakooste ja join-metodi lambda-lausekkeeseen voidaan esimerkiksi järjestää merkkijonot niistä löytyvien vokaalien mukaiseen järjestykseen välittämättä muista merkeistä:

mjonot = ["Mikko", "Makke", "Maija", "Markku", "Mikki"]

for jono in sorted(mjonot, key=lambda jono: "".join([m for m in jono if m in "aeiouyäö"])):
    print(jono)
Esimerkkitulostus

Makke Maija Markku Mikki Mikko

Anonyymejä funktioita voi hyödyntää Pythonissa monien muidenkin valmiiden funktioiden yhteydessä. Esimerkiksi funktioille min ja max voidaan määritellä samalla tavalla parametri key, jonka perusteella minimi- tai maksimiarvo valitaan.

Esimerkissä poimitaan levyistä aluksi vanhin ja sitten pisin:


class Levy:
    """Luokka mallintaa yhtä äänilevyä"""
    def __init__(self, nimi: str, esittaja: str, vuosi: int, kesto: int):
        self.nimi = nimi
        self.esittaja = esittaja
        self.vuosi = vuosi
        self.kesto = kesto


    def __str__(self):
        return f"{self.nimi} ({self.esittaja}), {self.vuosi}. {self.kesto} min."

if __name__ == "__main__":
    l1 = Levy("Nevermind", "Nirvana", 1991, 43)
    l2 = Levy("Let It Be", "Beatles", 1969, 35)
    l3 = Levy("Joshua Tree", "U2", 1986, 50)

    levyt = [l1, l2, l3]


    print("Vanhin levy:")
    print(min(levyt, key=lambda levy: levy.vuosi))

    print("Pisin levy: ")
    print(max(levyt, key=lambda levy: levy.kesto))
Esimerkkitulostus

Vanhin levy: Let It Be (Beatles), 1969. 35 min. Pisin levy: U2 (Joshua Tree), 1986. 50 min.

Loading

Funktiot parametreina omissa funktioissa

Pythonissa on siis mahdollista välittää viittaus johonkin funktioon toiselle funktiolle. Tarkastellaan vielä esimerkkinä omaa funktiota, joka saa parametrikseen toisen funktion:

# tyyppivihje callable viittaa funktioon
def suorita_operaatio(operaatio: callable):
    # Kutsutaan välitettyä funktiota
    return operaatio(10, 5)

def summa(a: int, b: int):
    return a + b

def tulo(a: int, b: int):
    return a * b


if __name__ == "__main__":
    print(suorita_operaatio(summa))
    print(suorita_operaatio(tulo))
    print(suorita_operaatio(lambda x,y: x - y))
Esimerkkitulostus

15 50 5

Funktion suorita_operaatio lopputulos siis riippuu siitä, mikä funktio sille on välitetty parametrina. Funktioksi kelpaa mikä tahansa funktio (niin def-lauseella määritelty kuin anonyymikin) jolla on kaksi parametria.

Vaikkei funktioiden välittäminen parametrina olekaan kaikkein yleisimmin tarvittava operaatio, on se joka tapauksessa hyödyllinen mekanismi. Esimerkiksi seuraava ohjelma kirjoittaa tiedostosta 1 halutut rivit tiedostoon 2. Rivien valintakriteeri annetaan funktiona, joka palauttaa True, jos rivi tulee kirjoittaa toiseen tiedostoon:

def kopioi_rivit(lahde_nimi: str, kohde_nimi: str, kriteeri= lambda x: True):
    with open(lahde_nimi) as lahde, open(kohde_nimi, "w") as kohde:
        for rivi in lahde:
            # Poistetaan ensin tyhjät merkit alusta ja lopusta
            rivi = rivi.strip()

            if kriteeri(rivi):
                kohde.write(rivi + "\n")

# Esimerkkejä
if __name__ == "__main__":
    # Jos kolmatta parametria ei ole määritelty, kopioidaan kaikki
    kopioi_rivit("eka.txt", "toka.txt")

    # Kopioidaan kaikki ei-tyhjät rivit
    kopioi_rivit("eka.txt", "toka.txt", lambda rivi: len(rivi) > 0)

    # Kopioidaan kaikki rivit, joilla on sana "Python"
    kopioi_rivit("eka.txt", "toka.txt", lambda rivi: "Python" in rivi)

    # Kopioidaan kaikki rivit, jotka eivät pääty pisteeseen
    kopioi_rivit("eka.txt", "toka.txt", lambda rivi: rivi[-1] != ".")

Funktiossa parametrille kriteeri on määritelty oletusarvoksi lambda-lauseke lambda x: True, jonka tuottama anonyymi funktio palauttaa arvon True kaikille syötteille. Niinpä oletuksena kopioidaan kaikki rivit tiedostosta toiseen. Jos käyttäjä antaa kolmannelle parametrille arvon, tämä korvaa oletusarvon.

Loading
Seuraava osa: