
🐍 Funktsionaalne programmeerimine Pythonis
Iga Pythoni arendaja kuuleb varem või hiljem: „proovi kirjutada funktsionaalses stiilis, su kood muutub puhtamaks." Aga kui sa avad dokumentatsiooni, näed sa map, filter ja reduce koos lambdadega ega saa aru, kust alustada.
Probleem pole selles, et funktsionaalne programmeerimine on keeruline. Probleem on selles, et enamik juhendeid kas sukelduvad akadeemilisse Haskellisse või piirduvad süntaksiga, selgitamata „miks". Python kui mitmeparadigmaline keel pakub täpselt nii palju funktsionaalseid tööriistu, kui igapäevaste ülesannete jaoks vaja, ilma äärmusteta.
Selles materjalis on praktiline ülevaade Pythoni funktsionaalsetest võimalustest: alates loendihõlmamistest kuni lambdafunktsioonideni, koos reaalsete näidete ja stsenaariumidega, kus iga tööriist koodi tõeliselt lihtsustab.
💡 Kiire ülevaade:
- Loendihõlmamiste lahtivõtmine, miks need on tsüklitest paremad ja millal neid kasutada
- Generaatorite valdamine:
yield, mälu kokkuhoid ja generaatoravaldised map+filter+reduceläbikäimine: praktilised näited tsüklite asendamiseks- Lambdafunktsioonide kirjutamise õppimine ja arusaamine, kus need on kohased ja kus kahjustavad loetavust
Funktsionaalne programmeerimine, millest me räägime
Funktsionaalne programmeerimine on lähenemine, kus programm ehitatakse üles matemaatilistest funktsioonidest: need võtavad muutumatud sisendandmed ja tagastavad tulemuse, muutmata midagi väljaspool. Ei mingeid kõrvalefekte, globaalse oleku muutmist pole.
Puhastes funktsionaalsetes keeltes nagu Haskell on programmi „puhtad" ja „ebapuhtad" (välismaailmaga suhtlevad) osad rangelt eraldatud. See lähenemine võimaldab koodi korrektsuse formaalset tõestamist: kompilaator garanteerib ootamatute kõrvalefektide puudumise.
Pythonit ei loodud funktsionaalse keelena. See on mitmeparadigmaline: objektorienteeritud, protseduuriline ja jah, ka funktsionaalne. Pythonis puhtalt funktsionaalses stiilis kirjutamine on nagu naelte löömine mikroskoobiga: tehniliselt võimalik, aga ebamugav ja ebaloomulik. Kuid üksikute funktsionaalsete tööriistade võtmine ja nende põimimine tuttavasse koodi on toimiv ja kasulik muster.
Loendihõlmamised, Pythoni peamine funktsionaalne tööriist
Funktsionaalse programmeerimise leib ja või on loenditega töötamine. Elementide valimine tingimuse järgi, iga elemendi teisendamine, uue loendi ehitamine olemasolevast, kõike seda katavad Pythonis loendihõlmamised.
Enne, protseduuriline tsükkel:
1 def filter_odd(li): 2 result = [] 3 for i in li: 4 if i % 2 == 1: 5 result.append(i) 6 return result 7 8 print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))
Pärast, loendihõlmamine:
1 li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31] 2 odd_numbers = [x for x in li if x % 2 == 1] 3 print(odd_numbers) # [7, 1, 19, 31]
Neli rida protseduurilist koodi suruti üheks kokku. Loetavus ei kannatanud: süntaks [expression for element in iterator if condition] on intuitiivselt arusaadav, „võta x li-st, kui x on paaritu".
Ja see pole lihtsalt süntaktiline suhkur. Loendihõlmamine töötab kiiremini kui samaväärne tsükkel koos .append()-iga, sest see käivitatakse C tasemel, mitte Pythoni interpretaatori tasemel.
Mis on iteraator
Loendihõlmamine tugineb iteraatori kontseptsioonile, objektile, mis tagastab next() päringu korral jada järgmise elemendi. Iga objekti, mis realiseerib __iter__ meetodi, nimetatakse itereeritavaks. Loend, sõne, ennik, sõnastik, hulk, need kõik on itereeritavad.
1 spam_iter = iter("foobar") 2 result = "".join([c.upper() for c in spam_iter]) 3 print(result) # FOOBAR
Siin tagastab iter() sõne iteraatori ja loendihõlmamine itereerib selle üle, rakendades igale märgile .upper().
Generaatorid, laisad jadad
Iteraatorit saab luua mitte ainult olemasolevast kollektsioonist, vaid ka generaatorfunktsiooni abil. return asemel kasutab see yield: funktsioon „uinub", jättes oma oleku meelde, ja järgmise next() kutse korral jätkab samast kohast.
1 def gen(max_val): 2 i = 1 3 while i < max_val: 4 yield i 5 i += 1 6 7 g = gen(1000) 8 for _ in range(12): 9 print(next(g))
See kood prindib numbrid 1 kuni 12. Funktsioon gen ei loo mällu tuhandest elemendist koosnevat loendit, see genereerib väärtusi ühekaupa, nõudmisel. Miljonite kirjetega jadade puhul (logifaili read, andmebaasivoog) on mälukulu erinevus suurusjärkudes.
Generaatoravaldised
Kompaktne süntaks: sama mis loendihõlmamised, kuid nurksulgude asemel ümarsulgudega.
1 g = (c.upper() for c in "foobar")
See pole ennik, see on generaator. Reegel on lihtne: nurksulud → loend (innukas, kõik mälus), ümarsulud → generaator (laisk, üks element korraga).
Generaatorid on sild protseduurilise ja funktsionaalse stiili vahel Pythonis: need pakuvad laiska väärtustamist ilma monaaditeooriasse sukeldumata.
Map, filter ja reduce: funktsionaalse töötluse kolm sammast
Kolm sisseehitatud funktsiooni, mis funktsionaalsetes keeltes on kõige alus. Python realiseerib need omal moel ja oluline on teada Pythoni-spetsiifiliste versioonide nüansse.
Map, rakenda funktsioon igale elemendile
1 def square(x): 2 return x * x 3 4 result = map(square, [1, 2, 3]) 5 print(list(result)) # [1, 4, 9]
map võtab funktsiooni ja jada ning tagastab iteraatori funktsiooni rakendamise tulemustega igale elemendile. Python 3-s tagastab map iteraatori, mitte loendi, seega on tulemuse nägemiseks vaja list().
Lihtsate teisenduste puhul on loendihõlmamine sageli loetavam:
1 [x * x for x in [1, 2, 3]] # same thing, but more familiar
Filter, vali elemendid tingimuse järgi
1 def is_upper(c): 2 return c == c.upper() 3 4 result = filter(is_upper, "FreedominObscureandoutlandishcOde") 5 print(list(result)) # ['F', 'O', 'O']
filter jätab alles ainult need elemendid, mille puhul predikaatfunktsioon tagastab True. Nagu map, tagastab see Python 3-s iteraatori.
Reduce, voldi jada üheks väärtuseks
Erinevalt map-ist ja filter-ist ei asu reduce sisseehitatud ulatuses, vaid functools moodulis. See rakendab funktsiooni järjest elementidele, akumuleerides tulemust.
1 from functools import reduce 2 3 def add(a, b): 4 return a + b 5 6 print(reduce(add, range(1, 6))) # 15
Kulisside taga: ((((1 + 2) + 3) + 4) + 5) = 15. Arvude summeerimiseks on Pythonis sisseehitatud sum(), seega kasutatakse reduce-i sagedamini mittestandardseteks voltimisteks: lame loend puuks ehitamine, pesastatud sõnastike ühendamine, jada suurima ühisteguri arvutamine.
Lambdafunktsioonid, anonüümsed abilised
Kui funktsiooni on vaja täpselt üks kord ja selle keha mahub ühte avaldisse, kasuta lambda-t:
1 lambda arguments: expression
Sama filter-i näite saab ümber kirjutada ilma eraldi def-ita:
1 result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))
Lambda on kompromiss. Pluss: pole vaja deklareerida eraldi funktsiooni ühe triviaalse kontrolli jaoks. Miinus: loetavus langeb, kui avaldis muutub keerukamaks kui paar operatsiooni. Rusikareegel: kui lambda ei mahu ühele reale või hakkad lambdasid üksteise sisse pesastama, eralda tavaline def.
Kus lambdad on tegelikult kasulikud
Kõige tavalisem stsenaarium on sortimine mittestandardse võtme järgi:
1 users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}] 2 users.sort(key=lambda u: u["age"])
Või kiire teisendus map/filter sees, kui keha on tõesti triviaalne:
1 squares = list(map(lambda x: x * x, range(10)))
Aga kui sirutad käe lambda järele nelja või viie operatsiooniga loogika jaoks, peatu. Funktsiooni nimi toimib dokumentatsioonina ja def pakub ruumi docstring'i jaoks.
⁉️🤔 Levinud küsimused
Millal on loendihõlmamine parem kui map?
Loendihõlmamine nagu
[x*2 for x in data]loeb loomulikumalt kuilist(map(lambda x: x*2, data)). Sisseehitatud operatsioon lambdaga omab kahte kaudsuse taset, samas kui loendihõlmamisel on üks. Aga kui funktsioon on juba eraldidef-ina olemas, onmapsellega lühike. Loendihõlmamine võidab loetavuses lihtsate teisenduste puhul, eriti filtreerimisega läbi[... if ...]. Keeruka loogika ja vahemuutujatega on tavalinefor-tsükkel ületamatu.
Generaator või loend, kumba valida?
Kui tulemust on vaja mitu korda (itereerimine, indekseerimine, pikkus), võta loend. Kui itereerid ühe korra ja andmemaht on suur, säästab generaator mälu. Miljonist täisarvust koosnev loend võtab umbes 8 MB, generaator sama vahemiku jaoks võtab vähem kui kilobaidi. Kuid generaatorit ei saa „tagasi kerida" ega indeksi järgi pöörduda. Praktiline reegel: andmed failist või andmebaasist, generaator; tulemus korduvaks kasutamiseks,
list().
Kas lambdad kahjustavad koodi loetavust?
Väikestes annustes ja triviaalse loogika puhul, ei.
key=lambda x: x["price"]loetakse hetkega. Aga niipea kui lambda lakkab olemast „üks mõte", eralda see tavaliseks funktsiooniks. Koodiülevaatusega meeskonnad piiravad lambdad ühe avaldisega ilma pesastuseta. Kui kolleeg vajab selle mõistmiseks rohkem kui viis sekundit, kirjuta see ümberdef-iks. Nimi toimib dokumentatsioonina:def by_price(item): return item["price"]on selgem kui anonüümnelambda.
Kui palju aeglasem on funktsionaalne Python võrreldes protseduurilisega?
Enamiku igapäevaste ülesannete puhul on erinevus tühine, mikrosekundi murdosad. Loendihõlmamine on sageli kiirem kui samaväärne tsükkel, sest see on realiseeritud C-s. Pythoni programmide pudelikael pole valik
map-i ja tsükli vahel, vaid sisend/väljund, võrgupäringud ja arhitektuur üldiselt. Kirjuta selgelt, profileeri kuumi kohticProfile-iga ja optimeeri ainult seda, mis tegelikult aeglustab.
Mida oma koodis täna kasutada
Pythoni funktsionaalsed tööriistad pole dogma, vaid hoovad. Need ei asenda protseduurilist ja objektorienteeritud koodi, vaid täiendavad seda konkreetsetes kohtades.
Alusta loendihõlmamistest, see on kõige tavalisem ja ohutum rakendus. Valda generaatoreid: niipea kui näed ajutist loendit, mida kasutatakse üks kord, küsi endalt, kas seda ei peaks asendama yield-iga? Kasuta map-i ja filter-it seal, kus need tõeliselt lihtsustavad teisendusahelat. Säästa lambdad key= jaoks sortimisel ja triviaalsete tagasikutsete jaoks.
Funktsionaalne stiil Pythonis ei seisne „puhtuses", vaid väljendusrikkuses. Kui pärast tsükli asendamist loendihõlmamisega muutus kood selgemaks, tegid sa kõik õigesti. Proovi üks probleemne meetod funktsionaalses võtmes ümber kirjutada ja võrdle diff-i: sageli räägib tulemus enda eest.



