Skip to content

Kaikki WordPressistä, web-kehityksestä — ja paljon muuta

🐍 Funktionaalinen ohjelmointi Pythonissa

🐍 Funktionaalinen ohjelmointi Pythonissa

Jokainen Python-kehittäjä kuulee ennemmin tai myöhemmin: "kokeile kirjoittaa funktionaalisella tyylillä, koodistasi tulee selkeämpää." Mutta kun avaat dokumentaation, näet map, filter ja reduce lambdojen kanssa etkä ymmärrä, mistä aloittaa.

Ongelma ei ole se, että funktionaalinen ohjelmointi olisi vaikeaa. Ongelma on se, että useimmat oppaat joko sukeltavat akateemisiin Haskell-viidakoihin tai rajoittuvat syntaksiin selittämättä "miksi". Python moniparadigmaisena kielenä tarjoaa juuri sen verran funktionaalisia työkaluja kuin tarvitset jokapäiväisiin tehtäviin, ilman äärimmäisyyksiä.

Tässä materiaalissa käytännönläheinen erittely Pythonin funktionaalisista ominaisuuksista: listakoosteista lambda-funktioihin, aitojen esimerkkien ja skenaarioiden kera, joissa kukin työkalu aidosti yksinkertaistaa koodia.

💡 Nopea yleiskatsaus:

  • Listakoosteiden purkaminen, miksi ne ovat parempia kuin silmukat ja milloin niitä kannattaa käyttää
  • Generaattoreiden hallinta: yield, muistin säästö ja generaattorilausekkeet
  • map + filter + reduce läpikäynti: käytännön esimerkkejä silmukoiden korvaamisesta
  • Lambda-funktioiden kirjoittamisen opettelu ja sen ymmärtäminen, missä ne ovat paikallaan ja missä ne heikentävät luettavuutta

Funktionaalinen ohjelmointi, mistä puhumme

Funktionaalinen ohjelmointi on lähestymistapa, jossa ohjelma rakennetaan matemaattisista funktioista: ne ottavat muuttumatonta syötetietoa ja palauttavat tuloksen muuttamatta mitään ulkopuolista. Ei sivuvaikutuksia, ei globaalin tilan muokkaamista.

Puhtaissa funktionaalisissa kielissä, kuten Haskellissa, ohjelman "puhtaat" ja "epäpuhtaat" (ulkoisen maailman kanssa vuorovaikuttavat) osat erotetaan tiukasti toisistaan. Tämä lähestymistapa mahdollistaa koodin oikeellisuuden muodollisen todistamisen: kääntäjä takaa odottamattomien sivuvaikutusten puuttumisen.

Pythonia ei suunniteltu funktionaaliseksi kieleksi. Se on moniparadigmainen: olio-, proseduraalinen ja kyllä, myös funktionaalinen. Pythonilla puhtaasti funktionaalisella tyylillä kirjoittaminen on kuin naulojen vasarointia mikroskoopilla: teknisesti mahdollista, mutta epämukavaa ja luonnotonta. Yksittäisten funktionaalisten työkalujen ottaminen ja upottaminen tuttuun koodiin on kuitenkin toimiva ja hyödyllinen malli.

Listakoosteet, Pythonin tärkein funktionaalinen työkalu

Funktionaalisen ohjelmoinnin jokapäiväinen leipä on listojen käsittely. Elementtien valinta ehdon perusteella, kunkin elementin muuntaminen, uuden listan rakentaminen olemassa olevasta, kaikki tämä hoituu Pythonissa listakoosteilla.

Ennen, proseduraalinen silmukka:

1def filter_odd(li):
2 result = []
3 for i in li:
4 if i % 2 == 1:
5 result.append(i)
6 return result
7
8print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))

Jälkeen, listakooste:

1li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31]
2odd_numbers = [x for x in li if x % 2 == 1]
3print(odd_numbers) # [7, 1, 19, 31]

Neljä riviä proseduraalista koodia tiivistettynä yhteen. Luettavuus ei kärsinyt: syntaksi [expression for element in iterator if condition] on intuitiivisesti selkeä, "ota x li:stä jos x on pariton".

Eikä tämä ole pelkkää syntaktista sokeria. Listakooste suoritetaan nopeammin kuin vastaava silmukka, jossa on .append(), koska se suoritetaan C-tasolla, ei Python-tulkin tasolla.

Mikä on iteraattori

Listakooste nojaa iteraattorin käsitteeseen, olioon, joka palauttaa sekvenssin seuraavan alkion, kun next() kutsutaan. Mitä tahansa oliota, joka toteuttaa __iter__-metodin, kutsutaan iteroitavaksi. Lista, merkkijono, monikko, sanakirja, joukko, ne kaikki ovat iteroitavia.

1spam_iter = iter("foobar")
2result = "".join([c.upper() for c in spam_iter])
3print(result) # FOOBAR

Tässä iter() palauttaa merkkijonoiteraattorin, ja listakooste käy sen läpi soveltaen .upper() jokaiseen merkkiin.

Generaattorit, laiskat sekvenssit

Iteraattorin voi luoda paitsi olemassa olevasta kokoelmasta, myös generaattorifunktion avulla. return-lauseen sijaan se käyttää yield-lausetta: funktio "nukahtaa" muistaen tilansa, ja seuraavalla next()-kutsulla jatkaa samasta kohdasta.

1def gen(max_val):
2 i = 1
3 while i < max_val:
4 yield i
5 i += 1
6
7g = gen(1000)
8for _ in range(12):
9 print(next(g))

Tämä koodi tulostaa numerot 1:stä 12:een. gen-funktio ei luo tuhannen alkion listaa muistiin, se tuottaa arvot yksi kerrallaan, tarpeen mukaan. Miljoonien tietueiden sekvensseille (lokitiedoston rivit, tietokantavirta) ero muistinkulutuksessa on kertaluokkia.

Generaattorilausekkeet

Kompakti syntaksi: sama kuin listakoosteissa, mutta kaarisulkeilla hakasulkeiden sijaan.

1g = (c.upper() for c in "foobar")

Tämä ei ole monikko, tämä on generaattori. Sääntö on yksinkertainen: hakasulkeet → lista (ahne, kaikki muistissa), kaarisulkeet → generaattori (laiska, yksi alkio kerrallaan).

Generaattorit ovat silta proseduraalisen ja funktionaalisen tyylin välillä Pythonissa: ne tarjoavat laiskan evaluoinnin ilman monaditeoriaan sukeltamista.

Map, filter ja reduce: funktionaalisen käsittelyn kolme pilaria

Kolme sisäänrakennettua funktiota, jotka funktionaalisissa kielissä ovat kaiken perusta. Python toteuttaa ne omalla tavallaan, ja on tärkeää tuntea Python-spesifien versioiden vivahteet.

Map, funktion soveltaminen jokaiseen alkioon

1def square(x):
2 return x * x
3
4result = map(square, [1, 2, 3])
5print(list(result)) # [1, 4, 9]

map ottaa funktion ja sekvenssin, ja palauttaa iteraattorin, jossa on tulokset funktion soveltamisesta jokaiseen alkioon. Python 3:ssa map palauttaa iteraattorin, ei listaa, joten tarvitset list() nähdäksesi tuloksen.

Yksinkertaisissa muunnoksissa listakooste on usein luettavampi:

1[x * x for x in [1, 2, 3]] # same thing, but more familiar

Filter, alkioiden valinta ehdon perusteella

1def is_upper(c):
2 return c == c.upper()
3
4result = filter(is_upper, "FreedominObscureandoutlandishcOde")
5print(list(result)) # ['F', 'O', 'O']

filter pitää vain ne alkiot, joille predikaattifunktio palauttaa True. Kuten map, se palauttaa iteraattorin Python 3:ssa.

Reduce, sekvenssin taittaminen yhdeksi arvoksi

Toisin kuin map ja filter, reduce ei asu sisäänrakennetussa nimiavaruudessa, vaan functools-moduulissa. Se soveltaa funktiota peräkkäin alkioihin kasvattaen tulosta.

1from functools import reduce
2
3def add(a, b):
4 return a + b
5
6print(reduce(add, range(1, 6))) # 15

Konepellin alla: ((((1 + 2) + 3) + 4) + 5) = 15. Lukujen summaamiseen Pythonissa on sisäänrakennettu sum(), joten reduce käytetään useammin epästandardeihin taitoksiin: puun rakentaminen litistetystä listasta, sisäkkäisten sanakirjojen yhdistäminen, sekvenssin suurimman yhteisen tekijän laskeminen.

Lambda-funktiot, anonyymit apurit

Kun funktiota tarvitaan tasan kerran ja sen runko mahtuu yhteen lausekkeeseen, käytä lambda:

1lambda arguments: expression

Sama filter-esimerkki voidaan kirjoittaa uudelleen ilman erillistä def-määrittelyä:

1result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))

Lambda on kompromissi. Plussaa: ei tarvitse määritellä erillistä funktiota yhtä triviaalia tarkistusta varten. Miinusta: luettavuus laskee, jos lausekkeesta tulee muutamaa operaatiota monimutkaisempi. Nyrkkisääntö: jos lambda ei mahdu yhdelle riville tai alat sisäkkäistää lambdoja toistensa sisään, erota tavallinen def.

Missä lambdat ovat oikeasti hyödyllisiä

Yleisin skenaario on lajittelu epästandardin avaimen mukaan:

1users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}]
2users.sort(key=lambda u: u["age"])

Tai nopea muunnos map/filter-funktioissa, kun runko on todella triviaali:

1squares = list(map(lambda x: x * x, range(10)))

Mutta jos olet tarttumassa lambdaan neljän tai viiden operaation logiikkaa varten, pysähdy. Funktion nimi toimii dokumentaationa, ja def tarjoaa tilaa docstringille.

⁉️🤔 Yleisiä kysymyksiä

Milloin listakooste on parempi kuin map?

Listakooste, kuten [x*2 for x in data], lukee luonnollisemmin kuin list(map(lambda x: x*2, data)). Sisäänrakennetussa operaatiossa lambdan kanssa on kaksi epäsuoruuden tasoa, kun taas listakoosteessa on yksi. Mutta jos funktio on jo olemassa erillisenä def-määrittelynä, map sen kanssa on ytimekäs. Listakooste voittaa luettavuudessa yksinkertaisissa muunnoksissa, erityisesti suodatuksessa [... if ...]-rakenteella. Monimutkaisessa logiikassa, jossa on välimuuttujia, tavallinen for-silmukka on lyömätön.

Generaattori vai lista, kumpi valita?

Jos tulosta tarvitaan useita kertoja (iterointi, indeksointi, pituus), ota lista. Jos iteroit kerran ja datamäärä on suuri, generaattori säästää muistia. Miljoonan kokonaisluvun lista vie noin 8 Mt, generaattori samalle välille vie alle kilotavun. Mutta generaattoria ei voi "kelata taaksepäin" eikä siihen voi viitata indeksillä. Käytännön sääntö: data tiedostosta tai tietokannasta, generaattori; tulos toistuvaan käyttöön, list().

Heikentävätkö lambdat koodin luettavuutta?

Pieninä annoksina ja triviaalissa logiikassa eivät. key=lambda x: x["price"] luetaan välittömästi. Mutta heti kun lambda lakkaa olemasta "yksi ajatus", erota se tavalliseksi funktioksi. Koodikatselmointia käyttävät tiimit rajoittavat lambdat yhteen lausekkeeseen ilman sisäkkäisyyttä. Jos kollegalta kestää yli viisi sekuntia ymmärtää se, kirjoita se uudelleen def-määrittelynä. Nimi toimii dokumentaationa: def by_price(item): return item["price"] on selkeämpi kuin anonyymi lambda.

Kuinka paljon hitaampaa funktionaalinen Python on kuin proseduraalinen?

Useimmissa jokapäiväisissä tehtävissä ero on merkityksetön, mikrosekunnin murto-osia. Listakooste on usein nopeampi kuin vastaava silmukka, koska se on toteutettu C:llä. Pullonkaula Python-ohjelmissa ei ole valinta map-funktion ja silmukan välillä, vaan syöte/tulostus, verkkopyynnöt ja arkkitehtuuri yleisesti. Kirjoita selkeästi, profiloi kuumat kohdat cProfile-työkalulla ja optimoi vain se, mikä oikeasti hidastaa.

Mitä käyttää koodissasi tänään

Pythonin funktionaaliset työkalut eivät ole dogmi, vaan vipuja. Ne eivät korvaa proseduraalista ja oliopohjaista koodia, vaan täydentävät sitä tietyissä kohdissa.

Aloita listakoosteista, tämä on yleisin ja turvallisin sovellus. Hallitse generaattorit: heti kun näet väliaikaisen listan, jota käytetään kerran, kysy itseltäsi, pitäisikö tämä korvata yield-lauseella? Käytä map ja filter siellä, missä ne aidosti yksinkertaistavat muunnosketjua. Säästä lambdat key=-parametriin lajittelussa ja triviaaleihin takaisinkutsuihin.

Funktionaalinen tyyli Pythonissa ei ole "puhtaudesta", vaan ilmaisuvoimasta. Jos silmukan korvaamisen jälkeen listakoosteella koodista tuli selkeämpää, teit kaiken oikein. Kokeile kirjoittaa yksi ongelmallinen metodi uudelleen funktionaalisella otteella ja vertaa diffiä: usein tulos puhuu puolestaan.