Koda funktionellt i Python: Lär dig kraften i det funktionella programmeringsparadigmet. Anonyma funktioner eller lambda

Det är inte för inte som Python-språket är populärt bland Googles programmerare och Hacker-redaktörer på samma gång :). Den här är verkligen kraftfullt språk låter dig skriva kod efter flera paradigm, och idag ska vi försöka ta reda på vad skillnaden är mellan dem och vilken som är bättre att följa.

Vilka paradigm?! Låt oss koda!

När du behöver skriva något är det sista du förmodligen oroar dig för vilket programmeringsparadigm du ska välja. Snarare väljer du antingen det mest lämpliga språket, eller börjar genast koda i din favorit, föredragen och beprövad genom åren. Det är sant, låt ideologer tänka på ideologi, vårt jobb är att programmera :). Och ändå, när du programmerar, följer du nödvändigtvis något slags paradigm. Låt oss titta på ett exempel. Låt oss försöka skriva något enkelt ... ja, till exempel, låt oss beräkna arean av en cirkel.

Du kan skriva det så här:

Area av en cirkel (alternativ ett)

dubbel area_of_circle(dubbel r) (
returnera M_PI*pow(r,2);
}
int main() (
dubbel r = 5;
cout<< "Площадь: "<< area_of_circle(r)<< endl;
}

Eller så kan du göra så här:

Area av en cirkel (alternativ två)

klass cirkel (
dubbel r;
offentlig:
Cirkel(dubbel r) (detta->r = r;)
double area() (retur M_PI*pow(this->r,2);)
void print_area() (
cout<< "Площадь: "<< this->område()<< endl;
}
};
int main() ((ny Circle(5))->print_area();)

Det kan göras annorlunda... men hur mycket du än försöker, kommer koden att vara antingen imperativ (som i det första fallet) eller objektorienterad (som i det andra).
Detta beror inte på brist på fantasi, utan helt enkelt på att C++ är skräddarsytt för dessa paradigm.

Och det bästa (eller värsta, beroende på hur raka händerna är) som du kan göra med det är att blanda flera paradigm.

Paradigm

Som du kanske har gissat kan du skriva på samma språk efter flera paradigm, ibland till och med flera samtidigt. Låt oss titta på deras huvudrepresentanter, för utan denna kunskap kommer du aldrig att kunna betrakta dig själv som en professionell kodare, och du kommer troligen att behöva glömma att arbeta i ett team.

Imperativ programmering

"Först gör vi det här, sedan det här, sedan det här"

Språk: Nästan alla

Ett paradigm som är absolut förståeligt för alla programmerare: "En person ger en uppsättning instruktioner till en maskin."
Alla börjar lära sig/förstå programmering från imperativparadigmet.

Funktionell programmering

"Vi beräknar uttrycket och använder resultatet till något annat."

Språk: Haskell, Erlang, F#

Ett paradigm helt obegripligt för en nybörjare. Vi beskriver inte en sekvens av tillstånd (som i imperativparadigmet), utan en sekvens av handlingar.

Objektorienterad programmering

"Vi utbyter meddelanden mellan objekt och simulerar interaktioner i den verkliga världen."

Språk: Nästan alla

Med sin tillkomst har det objektorienterade paradigmet kommit in i våra liv.
Nästan alla moderna affärsprocesser bygger på OOP.

Logisk programmering

"Vi svarar på frågan genom att hitta en lösning."

Språk: Prolog

Logisk programmering är en ganska specifik sak, men samtidigt intressant och intuitiv.
Ett enkelt exempel räcker:

(ställ reglerna)
häxa (X)<= burns(X) and female(X).
brännskador (X)<= wooden(X).
trä(X)<= floats(X).
flyter (X)<= sameweight(duck, X).
(vi sätter observationer)
kvinna (tjej).
samma vikt (anka, flicka).
(ställa en fråga)
? häxa (tjej).

Medan varje programmerare per definition är bekant med imperativ och objektorienterad programmering, möter vi sällan funktionell programmering i dess rena form.

Funktionell programmering kontrasteras med imperativ programmering.

Imperativ programmering involverar en sekvens av ändringar av tillståndet för ett program, och variabler används för att lagra detta tillstånd.

Funktionell programmering, tvärtom, involverar en sekvens av åtgärder på data. Detta är besläktat med matematik - vi skriver formeln f(x) på tavlan under en lång tid, och ersätter sedan x och får resultatet.

Och hela poängen med funktionell programmering är att här är formeln ett verktyg som vi tillämpar på X.

Tvåsidig python

Det finns ingen bättre teori än praktik, så låt oss skriva något redan. Ännu bättre, skriv det i Python :).
Låt oss beräkna summan av kvadraterna av elementen i "data"-arrayen imperativt och funktionellt:

Imperativ Python

data = [...]
summa = 0
för element i en:
summa += element ** 2
tryck summa

Funktionell Python

data = [...]
sq = lambda x: x**2
summa = lambda x,y: x+y
print reduce(summa, map(sq, data))

Båda exemplen finns i Python, även om jag inte inkluderade det i listan över funktionella språk. Detta är ingen tillfällighet, eftersom ett fullt fungerande språk är en ganska specifik och sällan använd sak. Det första funktionella språket var Lisp, men inte ens det var helt funktionellt (förbryllande, eller hur?). Fullt fungerande språk används för alla möjliga vetenskapliga tillämpningar och används ännu inte i stor utsträckning.

Men om själva "funktionaliteterna" inte blev utbredda migrerade vissa idéer från dem till skriptspråk (och inte bara) programmeringsspråk.
Det visade sig att det inte alls är nödvändigt att skriva fullt fungerande kod, det räcker med att dekorera imperativkoden med funktionella element.

Python i aktion

Det visar sig att begreppen FP är implementerade i Python mer än elegant. Låt oss ta en närmare titt på dem.

?-kalkyl

Lambdakalkyl är ett matematiskt begrepp som innebär att funktioner kan ta som argument och returnera andra funktioner.
Sådana funktioner kallas funktioner av högre ordning. ?-kalkyl är baserad på två operationer: tillämpning och abstraktion.
Jag har redan gett ett exempel på en applikation i föregående lista. Map- och reduceringsfunktionerna är samma högre ordningsfunktioner som "tillämpar", eller tillämpar, funktionen som skickas som ett argument till varje element i listan (för kartan) eller varje på varandra följande listelementpar (för reducera).

När det gäller abstraktion är det tvärtom: funktioner skapar nya funktioner baserat på deras argument.

Lambda abstraktion

def add(n):
retur lambda x: x + n

lägger till =

Här har vi skapat en lista med funktioner, som var och en lägger till ett visst antal till argumentet.
Det här lilla exemplet innehåller också ett par intressantare definitioner av funktionell programmering - closure och currying.

En stängning är definitionen av en funktion som beror på det interna tillståndet för en annan funktion. I vårt exempel är detta lambda x. Med denna teknik gör vi något som liknar att använda globala variabler, bara på lokal nivå.

Bärning är omvandlingen av en funktion som tar ett par argument till en funktion som tar sina argument ett i taget. Detta är vad vi gjorde i exemplet, bara vi slutade med en rad sådana funktioner.

På så sätt kan vi skriva kod som inte bara fungerar med variabler, utan också med funktioner, vilket ger oss några fler "frihetsgrader".

Rena funktioner och en lat kompilator

Imperativa funktioner kan ändra externa (globala) variabler, vilket innebär att en funktion kan returnera olika värden för samma argumentvärden vid olika stadier av programexekveringen.

Ett sådant påstående är inte alls lämpligt för det funktionella paradigmet. Här ses funktioner som matematiska, endast beroende på argument och andra funktioner, varför de kallas för "rena funktioner".

Som vi redan har tagit reda på kan du i det funktionella paradigmet hantera funktioner som du vill. Men vi får mest nytta när vi skriver "rena funktioner". En ren funktion är en funktion utan biverkningar, vilket innebär att den inte är beroende av sin omgivning och inte ändrar sitt tillstånd.

Att använda rena funktioner ger oss ett antal fördelar:

  • För det första, om funktioner inte är beroende av miljövariabler, minskar vi antalet fel som är associerade med oönskade värden av samma variabler. Tillsammans med antalet fel minskar vi också tiden det tar att felsöka programmet, och det är mycket lättare att felsöka sådana funktioner.
  • För det andra, om funktionerna är oberoende, har kompilatorn utrymme att ströva runt. Om en funktion bara beror på argument, kan den bara utvärderas en gång. Nästa gång kan du använda det cachade värdet. Dessutom, om funktionerna inte är beroende av varandra, kan de bytas ut och till och med automatiskt parallelliseras.

För att öka prestandan använder FP även lat utvärdering. Ett slående exempel:

print length()

I teorin bör vi få en division med noll fel vid utgången. Men en lat Python-kompilator kommer helt enkelt inte att beräkna värdena för varje element i listan, eftersom den inte ombads att göra det. Behöver listlängd – tack!
Samma principer gäller för andra språkkonstruktioner.

Som ett resultat får inte bara programmeraren utan också kompilatorn flera "frihetsgrader".

Lista uttryck och villkorliga uttalanden

För att livet (och programmeringen) inte ska verka som honung för dig, kom Python-utvecklare på en speciell "sötande" syntax, som bourgeoisin kallar "syntaktisk socker".
Det låter dig bli av med villkorliga uttalanden och loopar... ja, om inte bli av med dem, så reducera dem till ett minimum.

I princip såg du det redan i föregående exempel - det lägger till = . Här skapar och initialiserar vi omedelbart listan med funktionsvärden. Bekvämt, eller hur?
Det finns också en sådan sak som operatorerna och och eller, som låter dig klara dig utan besvärliga konstruktioner som om-elif-else.

Med hjälp av Python-verktygssatsen kan du alltså förvandla en besvärlig imperativ kod till en vacker funktionell.

Imperativ kod

L=
för x i xrange(10):
om x % 2 == 0:
om x**2>=50:
L.append(x)
annan:
L.append(-x)
tryck L

Funktionskod

skriva ut

Resultat

Som du redan förstår är det inte nödvändigt att helt följa det funktionella paradigmet, det räcker att skickligt använda det i kombination med det imperativa för att förenkla ditt liv. Men jag fortsatte att prata om imperativparadigmet... och sa ingenting om OOP och FP.

Tja, OOP är i själva verket en överbyggnad ovanpå imperativparadigmet, och om du har gått från IP till OOP, så borde nästa steg vara att tillämpa FP i OOP. Avslutningsvis kommer jag att säga några ord om abstraktionsnivån. Så ju högre den är, desto bättre, och det är kombinationen av OOP och FP som ger oss denna nivå.

CD

På disken lägger jag färska Python-distributioner för Windows-användare. Linux-människor behöver inte hjälp :).

WWW

Några bra resurser för dig som vill lära dig mer:

INFO

Om du inte gillar Python, oroa dig inte - du kan framgångsrikt tillämpa funktionella programmeringsidéer på andra högnivåspråk.

Det finns flera paradigm inom programmering, till exempel OOP, funktionell, imperativ, logisk, och många av dem. Vi kommer att prata om funktionell programmering.

Förutsättningarna för fullfjädrad funktionsprogrammering i Python är: högre ordningsfunktioner, utvecklade listbearbetningsverktyg, rekursion och förmågan att organisera lata beräkningar.

Idag kommer vi att bekanta oss med enkla element, och komplexa mönster kommer att vara i andra lektioner.

Teori i teorin

Precis som med OOP och funktionell programmering försöker vi undvika definitioner. Ändå är det svårt att ge en tydlig definition, så det kommer inte att finnas en tydlig definition här. Dock! Låt oss lyfta fram önskemålen om ett funktionellt språk:

Det här är inte en komplett lista, men även detta är tillräckligt för att göra den "vacker". Om läsaren vill ha mer, här är en utökad lista:

  • Funktioner av högre ordning
  • Rena funktioner
  • Oföränderlig data
  • Stängningar
  • Lättja
  • Svansrekursion
  • Algebraiska datatyper
  • Mönstermatchning

Låt oss gradvis överväga alla dessa punkter och hur man använder dem i Python.

Och idag, kortfattat, vad är vad på den första listan.

Rena funktioner

Rena funktioner ger inga observerbara biverkningar, bara returnera ett resultat. De ändrar inte globala variabler, skickar eller skriver inte ut något någonstans, rör inte objekt och så vidare. De accepterar data, beräknar något, tar bara hänsyn till argumenten och returnerar ny data.

  • Lättare att läsa och förstå kod
  • Lättare att testa (inget behov av att skapa "villkor")
  • Mer tillförlitliga eftersom de inte beror på "vädret" och miljöns tillstånd, bara på argumenten
  • Kan köras parallellt, resultat kan cachas

Oföränderlig data

Oföränderliga datastrukturer är samlingar som inte kan ändras. Nästan som siffror. Numret bara existerar, det kan inte ändras. Likaså är en oföränderlig array hur den skapades och kommer alltid att vara så. Om du behöver lägga till ett element måste du skapa en ny array.

Fördelar med oföränderliga strukturer:

  • Dela säkert referens mellan trådar
  • Lätt att testa
  • Lätt att spåra livscykeln (motsvarar dataflödet)

Funktioner av högre ordning

En funktion som tar en annan funktion som argument och/eller returnerar en annan funktion anropas högre ordningsfunktion:

Def f(x): return x + 3 def g(funktion, x): return funktion(x) * funktion(x) print(g(f, 7))

Efter att ha övervägt teorin, låt oss börja gå vidare till praktiken, från enkel till komplex.

Lista inneslutningar eller listgenerator

Låt oss titta på en språkdesign som hjälper till att minska antalet rader kod. Det är inte ovanligt att bestämma nivån på en Python-programmerare med denna konstruktion.

Exempelkod:

För x i xrange(5, 10): om x % 2 == 0: x =* 2 annat: x += 1

En cykel med ett tillstånd som detta är inte ovanligt. Låt oss nu försöka förvandla dessa 5 rader till en:

>>>

Inte illa, 5 rader eller 1. Dessutom har uttrycksförmågan ökat och sådan kod är lättare att förstå - en kommentar kan läggas till i alla fall.

I allmänhet är denna design som följer:

Det är värt att förstå att om koden inte är läsbar alls, är det bättre att överge en sådan design.

Anonyma funktioner eller lambda

Vi fortsätter att minska mängden kod.

Def calc(x, y): returnera x**2 + y**2

Funktionen är kort, och minst 2 rader var bortkastade. Går det att förkorta så små funktioner? Eller kanske inte formatera det som funktioner? När allt kommer omkring vill du inte alltid skapa onödiga funktioner i en modul. Och om funktionen tar upp en rad, så ännu mer. Därför finns det i programmeringsspråk anonyma funktioner som inte har ett namn.

Anonyma funktioner i Python implementeras med lambda-kalkyl och ser ut som lambda-uttryck:

>>> lambda x, y: x**2 + y**2 vid 0x7fb6e34ce5f0>

För en programmerare är det samma funktioner och du kan också arbeta med dem.

För att komma åt anonyma funktioner flera gånger tilldelar vi dem till en variabel och använder dem till vår fördel.

>>> (lambda x, y: x**2 + y**2)(1, 4) 17 >>> >>> func = lambda x, y: x**2 + y**2 >>> func(1, 4) 17

Lambdafunktioner kan fungera som ett argument. Även för andra lambdas:

Multiplikator = lambda n: lambda k: n*k

Använder lambda

Vi lärde oss hur man skapar funktioner utan namn, men nu ska vi ta reda på var de ska användas. Standardbiblioteket tillhandahåller flera funktioner som kan ta en funktion som ett argument - map(), filter(), reduce(), application().

Karta()

Map()-funktionen bearbetar en eller flera sekvenser med den givna funktionen.

>>> list1 = >>> list2 = [-1, 1, -5, 4, 6] >>> list(map(lambda x, y: x*y, list1, list2)) [-7, 2, -15, 40, 72]

Vi har redan blivit bekanta med listgeneratorn, låt oss använda den om längden på listan är densamma):

>>> [-7, 2, -15, 40, 72]

Så det märks att användningen av listinneslutningar är kortare, men lambdas är mer flexibla. Låt oss gå längre.

filtrera()

Filter()-funktionen låter dig filtrera värdena för en sekvens. Den resulterande listan innehåller endast de värden för vilka funktionsvärdet för elementet är sant:

>>> siffror = >>> list(filter(lambda x: x< 5, numbers)) # В результат попадают только те элементы x, для которых x < 5 истинно

Samma sak med listuttryck:

>>> siffror = >>>

minska()

För att organisera kedjeberäkningar i en lista kan du använda reduce()-funktionen. Till exempel kan produkten av elementen i en lista beräknas så här (Python 2):

>>> siffror = >>> reduce(lambda res, x: res*x, siffror, 1) 720

Beräkningar sker i följande ordning:

((((1*2)*3)*4)*5)*6

Samtalskedjan länkas med hjälp av ett mellanresultat (res). Om listan är tom används den tredje parametern helt enkelt (i fallet med en produkt av nollfaktorer är detta 1):

>>> reduce(lambda res, x: res*x, , 1) 1

Naturligtvis är mellanresultatet inte nödvändigtvis en siffra. Detta kan vara vilken annan datatyp som helst, inklusive en lista. Följande exempel visar baksidan av en lista:

>>> reduce(lambda res, x: [x]+res, , )

Python har inbyggda funktioner för de vanligaste operationerna:

>>> siffror = >>> summa(tal) 15 >>> lista(omvänd(tal))

Python 3 har ingen inbyggd reduce() funktion, men den finns i modulen functools.

tillämpa()

En funktion för att tillämpa en annan funktion på positionella och namngivna argument, given lista och en ordbok i enlighet därmed (Python 2):

>>> def f(x, y, z, a=Ingen, b=Ingen): ... skriv ut x, y, z, a, b ... >>> tillämpa(f, , ("a": 4, "b": 5)) 1 2 3 4 5

I Python 3 istället tillämpa funktioner() den speciella syntaxen ska användas:

>>> def f(x, y, z, a=Ingen, b=Ingen): ... print(x, y, z, a, b) ... >>> f(*, **(" a": 4, "b": 5)) 1 2 3 4 5

Låt oss avsluta recensionen med denna inbyggda funktion. standardbibliotek och låt oss gå vidare till det sista funktionella tillvägagångssättet för idag.

Stängningar

Funktioner som definieras i andra funktioner är stängningar. Varför är detta nödvändigt? Låt oss titta på ett exempel för att förklara:

Kod (fiktiv):

Def processing(element, type_filter, all_data_size): filters = Filter(all_data_size, type_filter).get_all() för filt in filter: element = filt.filter(element) def main(): data = DataStorage().get_all_data() för x i data: processing(x, "alla", len(data))

Vad du kan märka i koden: i den här koden finns variabler som i huvudsak lever permanent (dvs. identiska), men samtidigt laddar eller initierar vi flera gånger. Som ett resultat kommer vi till insikten att variabelinitiering tar upp lejonparten av tiden i denna process, det händer att även inläsning av variabler i omfånget minskar prestandan. För att minska överliggande användningsstängningar.

Stängningen initierar variabler en gång, som sedan kan användas utan overhead.

Låt oss lära oss hur man skapar stängningar:

Def multiplikator(n): "multiplikator(n) returnerar en funktion som multiplicerar med n" def mul(k): return n*k return mul # samma effekt kan uppnås med # multiplikator = lambda n: lambda k: n* k mul2 = multiplikator(2) # mul2 - funktion som multiplicerar med 2, till exempel mul2(5) == 10

Slutsats

På lektionen tittade vi på grundläggande koncept FP, och sammanställde även en lista över mekanismer som kommer att diskuteras i följande lektioner. Vi pratade om sätt att minska mängden kod, som listinneslutningar (listgenerator), lamda-funktioner och deras användning, och till sist var det några ord om stängningar och vad de är till för.

Existerar Ett stort antal publikationer som ägnas åt implementeringen av funktionella programmeringskoncept i Python, men det mesta av detta material skrevs av en författare - David Mertz. Dessutom är många av dessa artiklar redan föråldrade och utspridda över olika onlineresurser. I den här artikeln kommer vi att försöka återkomma till det här ämnet för att uppdatera och organisera tillgänglig information, särskilt med tanke på de stora skillnaderna som finns mellan rad 2 och rad 3 versioner av Python.

Funktioner i Python

Funktioner i Python definieras på två sätt: genom definition def eller via anonym beskrivning lambda. Båda dessa definitionsmetoder är tillgängliga, i varierande grad, i vissa andra programmeringsspråk. En egenskap hos Python är att en funktion är ett namngivet objekt precis som alla andra objekt av någon datatyp, säg en heltalsvariabel. Lista 1 visar enklaste exemplet(fil func.py från arkivet python_functional.tgz

Lista 1. Funktionsdefinitioner
#!/usr/bin/python # -*- kodning: utf-8 -*- import sys def show(fun, arg): print("() : ()".format(type(fun), fun)) print("arg=() => fun(arg)=()".format(arg, fun(arg))) if len(sys.argv) > 1: n = float(sys.argv[ 1 ]) else : n = float(input("nummer?: ")) def pow3(n): # 1:a funktionsdefinition return n * n * n show(pow3, n) pow3 = lambda n: n * n * n # 2 -th definition av en funktion med samma namn show(pow3, n) show((lambda n: n * n * n), n) # 3:e, användning av anonym funktionsdefinition

När vi anropar alla tre funktionsobjekten får vi samma resultat:

$ python func.py 1.3 : arg=1,3 => kul(arg)=2,197 : vid 0xb7662bc4> arg=1.3 => fun(arg)=2.197 : vid 0xb7662844> arg=1.3 => fun(arg)=2.197

Detta är ännu mer uttalat i Python version 3, där allt är en klass (inklusive en heltalsvariabel), och funktioner är programobjekt som tillhör en klass fungera:

$python3 func.py 1.3 : arg=1.3 => fun(arg)=2.197000000000000005 : vid 0xb745432c> arg=1.3 => fun(arg)=2.19700000000000005 : vid 0xb74542ec> arg=1.3 => fun(arg)=2.19700000000000005

Notera. Det finns ytterligare två typer av objekt som tillåter ett funktionsanrop - en funktionsklassmetod och en funktor, som vi kommer att prata om senare.

Om Python-funktionsobjekt är objekt precis som andra dataobjekt, kan du göra med dem allt du kan göra med vilken data som helst:

  • dynamiskt förändra pågående;
  • bädda in i mer komplexa datastrukturer (samlingar);
  • passera som parametrar och returvärden osv.

Detta (manipulation med funktionella objekt som dataobjekt) är vad funktionell programmering bygger på. Python är naturligtvis inte ett riktigt funktionellt programmeringsspråk, till exempel finns det speciella språk för fullt fungerande programmering: Lisp, Planner och nyare: Scala, Haskell. Ocaml, ... Men i Python kan du "bädda in" funktionella programmeringstekniker i det allmänna flödet av imperativ (kommando) kod, till exempel använda metoder lånade från fullfjädrade funktionella språk. De där. "vika" enskilda fragment av imperativ kod (ibland ganska stora) till funktionella uttryck.

Ibland frågar folk: "Vilka är fördelarna med den funktionella stilen att skriva enskilda fragment för en programmerare?" Den största fördelen med funktionell programmering är att efter att ha felsökt ett sådant fragment en gång, den efterföljande återanvändbar Det blir inga fel på grund av biverkningar i samband med uppdrag och namnkonflikter.

Ganska ofta vid programmering i Python används typiska konstruktioner från området funktionell programmering, till exempel:

skriv ut ([ (x,y) för x i (1, 2, 3, 4, 5) \ för y in (20, 15, 10) \ om x * y > 25 och x + y< 25 ])

Som ett resultat av löpningen får vi:

$ python funcp.py [(2,20), (2,15), (3,20), (3,15), (3,10), (4,20), (4,15), (4) ,10), (5,15), (5,10)]

Fungerar som objekt

När du skapar ett funktionsobjekt med en operator lambda, som visas i lista 1, kan du binda det skapade funktionsobjektet till namnet pow3 på exakt samma sätt som man skulle kunna fästa ett nummer till detta namn 123 eller sträng "Hallå!". Det här exemplet bekräftar statusen för funktioner som förstklassiga objekt i Python. En funktion i Python är bara ytterligare ett värde som du kan göra något med.

Den vanligaste åtgärden som görs med förstklassiga funktionsobjekt är att skicka dem till högre ordnings inbyggda funktioner: Karta(), minska() Och filtrera(). Var och en av dessa funktioner tar ett funktionsobjekt som sitt första argument.

  • Karta() tillämpar den godkända funktionen på varje element i de godkända listan/listorna och returnerar en lista med resultat (samma dimension som indata);
  • minska() tillämpar den godkända funktionen på varje värde i listan och på den interna resultatackumulatorn, t.ex. reduce(lambda n,m: n * m, range(1, 10)) betyder 10! (faktoriell);
  • filtrera() tillämpar den godkända funktionen på varje element i listan och returnerar en lista över de element i den ursprungliga listan för vilka den godkända funktionen returnerade sant.

Genom att kombinera dessa tre funktioner kan du implementera ett förvånansvärt brett utbud av kontrollflödesoperationer utan att tillgripa imperativa uttalanden, utan endast använda funktionella uttryck, som visas i Listing 2 (fil funcH.py från arkivet python_functional.tgz

Lista 2. Python funktioner av högre ordning
#!/usr/bin/python # -*- kodning: utf-8 -*- import sys def input_arg(): global arg arg = (lambda: (len(sys.argv) > 1 och int(sys.argv[ 1 ])) eller \ int(input("nummer?: ")))() return arg print("argument = ()".format(input_arg())) print(list(map(lambda x: x + 1) , range(arg))) print(list(filter(lambda x: x > 4, range(arg)))) import functools print("()! = ()".format(arg, functools.reduce(lambda x , y: x * y, range(1, arg))))

Notera. Den här koden är något mer komplex än det föregående exemplet på grund av följande kompatibilitetsproblem mellan Python version 2 och 3:

  • Fungera minska(), deklarerad som inbyggd i Python 2, flyttades till en modul i Python 3 funktionsverktyg och att kalla den direkt vid namn kommer att skapa ett undantag Namnfel, så för att det ska fungera korrekt måste anropet formateras som i exemplet eller inkludera raden: från functools import *
  • Funktioner Karta() Och filtrera() i Python 3 returnerar de inte en lista (som redan visades när man diskuterade versionsskillnader), utan iteratorobjekt av formen:

För att få hela listan med värden för dem, anropas funktionen lista().

Därför kommer den här koden att kunna fungera i båda versionerna av Python:

$python3 funcH.py 7 argument = 7 7! = 720

Om kodportabilitet mellan olika versioner inte krävs, kan sådana fragment uteslutas, vilket gör att koden kan förenklas något.

Rekursion

I funktionell programmering är rekursion en grundläggande mekanism, liknande loopar i iterativ programmering.

I vissa diskussioner om Python har jag upprepade gånger stött på påståenden om att i Python begränsas rekursionsdjupet av "hårdvara", och därför kan vissa åtgärder i princip inte genomföras. Python har en standardrekursionsdjupgräns på 1000, men detta är en numerisk inställning som alltid kan åsidosättas, som visas i Listing 3 (för den fullständiga exempelkoden, se filen fact2.py från arkivet python_functional.tgz

Listning 3. Beräknar faktorial med godtyckligt rekursionsdjup
#!/usr/bin/python # -*- kodning: utf-8 -*- import sys arg = lambda: (len(sys.argv) > 1 och int(sys.argv[ 1 ])) eller \ int( input("nummer?: ")) factorial = lambda x: ((x == 1) och 1) eller x * factorial(x - 1) n = arg() m = sys.getrecursionlimit() om n >= m - 1: sys.setrecursionlimit(n + 2) print("rekursionsdjupet överskrider det som är inställt i systemet (), återställ till ()".\ format(m, sys.getrecursionlimit())) print("n=( ) => n!=()".format(n, factorial(n))) if sys.getrecursionlimit() > m: print("rekursionsdjupet återställt till ()".format(m)) sys.setrecursionlimit(m )

Så här ser utförandet av det här exemplet ut i Python 3 och Python2 (även om det resulterande numret i verkligheten är osannolikt att få plats på en konsolterminalskärm):

$ python3 fact2.py 1001 rekursionsdjup överstiger systemuppsättning 1000, återställ till 1003 n=1001 => n!=4027........................ . ........................0000000000000 rekursionsdjup återställt till 1000

Några enkla exempel

Låt oss utföra flera enkla transformationer av den vanliga imperativkoden (kommando, operatör) för att omvandla dess individuella fragment till funktionella. Först, låt oss ersätta grenoperatorer med logiska villkor, som på grund av "uppskjutna" (lata, lata) beräkningar tillåter oss att kontrollera exekvering eller icke-exekvering av individuella kodgrenar. Alltså den imperativa konstruktionen:

om<условие>: <выражение 1>annan:<выражение 2>

Helt likvärdig med följande funktionella fragment (på grund av de "uppskjutna" funktionerna hos logiska operatorer och Och eller):

# funktion utan parametrar: lambda: (<условие>och<выражение 1>) eller (<выражение 2>)

Låt oss använda faktorberäkning igen som ett exempel. Lista 4 visar funktionskoden för beräkning av faktorn (fil fakta1.py i arkivet python_functional.tgz i avsnittet "Ladda ner material"):

Listning 4. Operatörs (imperativ) definition av faktoriell
#!/usr/bin/python # -*- kodning: utf-8 -*- import sys def factorial(n): if n == 1: return 1 else: return n * factorial(n - 1) if len( sys.argv) > 1: n = int(sys.argv[ 1 ]) annat: n = int(input("nummer?: ")) print("n=() => n!=()". format (n, faktoriell(n)))

Argumentet som ska beräknas härleds från parametervärdet kommandorad(om det finns) eller in från terminalen. Den första ändringen som visas ovan tillämpas redan i Lista 2, där funktionsuttryck har ersatts med:

  • Definition av faktorfunktionen: factorial = lambda x: ((x == 1) och 1) eller x * factorial(x - 1)
  • begäran att ange ett argumentvärde från terminalkonsolen: arg = lambda: (len(sys.argv) > 1 och int(sys.argv[ 1 ])) eller \ int(input("nummer?: ")) n = arg()

I fil fact3.py en annan definition av en funktion dyker upp, gjord genom en högre ordningsfunktion minska():

faktoriell = faktoriell = lambda z: reducera(lambda x, y: x * y, range(1, z + 1))

Här ska vi också förenkla uttrycket för n, reducera det till ett enda anrop till en anonym (ej namngiven) funktion:

n = (lambda: (len(sys.argv) > 1 och int(sys.argv[ 1 ])) eller \ int(input("nummer?: ")))()

Slutligen kan du lägga märke till att tilldela ett värde till en variabel n krävs endast för användning i ett samtal skriva ut() för att mata ut detta värde. Om vi ​​överger denna begränsning kommer hela applikationen att degenerera till en funktionell operatör (se filen fact4.py i arkivet python_functional.tgz i avsnittet "Ladda ner material"):

från sys import * från functools import reduce print("beräknad faktor = ()".format(\ (lambda z: reducer(lambda x, y: x * y, range(1, z + 1))) \ ((lambda) : (len(argv) > 1 och int(argv[ 1 ])) eller \ int(input("nummer?: ")))())))

Detta enda anrop i en funktion skriva ut() och representerar hela applikationen i dess funktionella form:

$python3 fact4.py nummer?: 5 beräknade faktorial = 120

Läser den här koden (fil fact4.py) bättre än imperativnotationen (fil fact1.py)? Mer troligt nej än ja. Vad är då dess värdighet? Faktum är att när någraändringar av omgivande kod, normal drift detta fragment kommer att bevaras, eftersom det inte finns någon risk för biverkningar på grund av förändringar i värdena för de variabler som används.

Funktioner av högre ordning

I en funktionell programmeringsstil är standardpraxis dynamisk generation ett funktionellt objekt under kodexekvering, med dess efterföljande anrop i samma kod. Det finns ett antal områden där en teknik som denna kan vara användbar.

Stängning

Ett av de intressanta koncepten med funktionell programmering är stängningar(stängning). Den här idén visade sig vara så frestande för många utvecklare att den till och med implementerades i vissa icke-funktionella programmeringsspråk (Perl). David Mertz ger följande definition av en stängning: "En stängning är en procedur tillsammans med en uppsättning data kopplad till den" (i motsats till objekt i objektprogrammering, som: "data tillsammans med en uppsättning procedurer kopplade till det") .

Innebörden av en stängning är att definitionen av en funktion "fryser" kontexten som omger den för ögonblick av beslutsamhet. Detta kan göras på olika sätt, till exempel genom att parametrisera funktionsskapandet, som visas i Lista 5 (fil clos1.py i arkivet python_functional.tgz i avsnittet "Ladda ner material"):

Notering 5. Skapa en stängning
# -*- kodning: utf-8 -*- def multiplikator(n): # multiplikator returnerar en funktion som multiplicerar med n def mul(k): return n * k return mul mul3 = multiplikator(3) # mul3 är en funktion som multipliceras med 3 print(mul3(3), mul3(5))

Så här fungerar en sådan dynamiskt definierad funktion:

$ python clos1.py (9, 15) $ python3 clos1.py 9 15

Ett annat sätt att skapa en stängning är att använda standardparametervärdet vid funktionsdefinitionspunkten, som visas i Lista 6 (fil clos3.py från arkivet python_functional.tgz i avsnittet "Ladda ner material"):

Lista 6. Ett annat sätt att skapa en stängning
n = 3 def mult(k, mul = n): return mul * k n = 7 print(mult(3)) n = 13 print(mult(5)) n = 10 mult = lambda k, mul=n: mul * k print(mult(3))

Inga efterföljande tilldelningar av standardparametern kommer att ändra den tidigare definierade funktionen, men själva funktionen kan åsidosättas:

$ python clos3.py 9 15 30

Delfunktionsapplikation

Delfunktion ansökan antar baserat på funktion N variabler definition av en ny funktion med färre variabler M < N, medan resten N-M variabler får fasta "frysta" värden (modulen används funktionsverktyg). Ett liknande exempel kommer att diskuteras nedan.

Funktionär

En funktor är inte en funktion, utan ett klassobjekt där en metod anropar __ring upp__(). I det här fallet kan ett anrop appliceras på en instans av ett sådant objekt, precis som det händer för funktioner. I lista 7 (fil part.py från arkivet python_functional.tgz(se avsnittet Nedladdningar) visar hur man använder en stängning, en partiell funktionsdefinition och en funktion för att producera samma resultat.

Lista 7. Jämföra stängning, partiell definition och funktion
# -*- kodning: utf-8 -*- def multiplikator(n): # closure def mul(k): return n * k return mul mul3 = multiplikator(3) från functools import partiell def mulPart(a, b ): # partiell tillämpning av funktionen return a * b par3 = partial(mulPart, 3) class mulFunctor: # ekvivalent funktion def __init__(self, val1): self.val1 = val1 def __call__(self, val2): return self.val1 * val2 fun3 = mulFunctor(3) print("() . () . ()".format(mul3(5), par3(5), fun3(5)))

Att anropa alla tre konstruktionerna för ett argument lika med 5 kommer att ge samma resultat, även om de kommer att använda helt olika mekanismer:

$ python part.py 15 . 15 . 15

Karring

Curring (eller currying) är omvandlingen av en funktion från många variabler till en funktion som tar sina argument ett i taget.

Notera. Denna transformation introducerades av M. Sheinfinkel och G. Frege och fick sitt namn för att hedra matematikern Haskell Curry, efter vilken programmeringsspråket Haskell också är uppkallat.

Bärning gäller inte unika funktioner funktionell programmering, så currying transformation kan skrivas till exempel i Perl eller C++. Curryoperatören är till och med inbyggd i vissa programmeringsspråk (ML, Haskell), vilket gör att funktioner på flera platser kan resultera i en curry representation. Men alla språk som stöder stängningar låter dig skriva curry-funktioner, och Python är inget undantag i detta avseende.

Lista 8 visar ett enkelt exempel med currying (fil curry1.py i arkivet python_functional.tgz i avsnittet "Ladda ner material"):

Notering 8. Curring
# -*- kodning: utf-8 -*- def spam(x, y): print("param1=(), param2=()".format(x, y)) spam1 = lambda x: lambda y: spam (x, y) def spam2(x) : def new_spam(y) : returnera spam(x, y) returnera new_spam spam1(2)(3) # currying spam2(2)(3)

Så här ser utförandet av dessa samtal ut:

$ python curry1.py param1=2, param2=3 param1=2, param2=3

Slutsats

Den här artikeln presenterade några möjligheter Python språk, så att du kan använda den för att skriva program med den funktionella programmeringsstilen. Så vi beskrev de grundläggande teknikerna för funktionell programmering och visade exempel på deras implementering i Python. Liksom i tidigare artiklar är kodexemplen skrivna på ett sådant sätt att de framgångsrikt kan köras och exekveras i båda versionerna av Python.

I nästa artikel kommer vi att diskutera frågorna om att organisera parallell exekvering av kod i Python-miljön.

3.2.3 Ordboksförståelser

Säg att vi har en ordbok vars nycklar är tecken och värdena som hänför sig till antalet gånger som tecknet förekommer i viss text. Ordboken skiljer för närvarande mellan versaler och gemener.

Vi kräver en ordbok där förekomsten av versaler och gemener kombineras:

dct = ( "a" : 10 , "b" : 34 , "A" : 7 , "Z" : 3 )

frekvens = ( k . lägre ( ) : dct . get ( k . lägre ( ) , 0 ) + dct . get ( k . övre ( ) , 0 )

för k i dct . nycklar())

utskriftsfrekvens #("a": 17, "z": 3, "b": 34)

Python stöder skapandet av anonyma funktioner (dvs funktioner som inte är bundna till ett namn) under körning, med hjälp av en konstruktion som kallas "lambda". Detta är inte exakt samma sak som lambda i funktionella programmeringsspråk, men det är ett mycket kraftfullt koncept som är väl integrerat i Python och används ofta i kombination med typiska funktionella koncept som filter(), map() och reduce() .

Anonyma funktioner i form av ett uttryck kan skapas med hjälp av lambda
påstående:

args är en kommaseparerad lista med argument, och expression är ett uttryck som involverar dessa argument. Den här koden visar skillnaden mellan en normal funktionsdefinition och en lambdafunktion:

def funktion (x):

returnera x * x

utskriftsfunktion (2) #4

#-----------------------#

funktion = lambda x : x * x

utskriftsfunktion (2) #4

Som du kan se gör båda function() exakt samma sak och kan användas på samma sätt. Observera att lambda-definitionen inte innehåller en "return"-sats - den innehåller alltid ett uttryck som returneras. Observera också att du kan lägga en lambdadefinition var som helst där en funktion förväntas, och du behöver inte tilldela den till en variabel alls.

Följande kodfragment visar användningen av lambda-funktioner.

def inkrement (n):

retur lambda x : x + n

utskriftssteg (2) # vid 0x022B9530>

utskriftssteg (2) (20) #22

Ovanstående kod definierar ett funktionsinkrement som skapar en anonym funktion i farten och returnerar den. Den returnerade funktionen ökar dess argument med det värde som angavs när den skapades.

Du kan nu skapa flera olika inkrementfunktioner och tilldela dem till variabler och sedan använda dem oberoende av varandra. Som det sista påståendet visar, behöver du inte ens tilldela funktionen någonstans - du kan bara använda den direkt och glömma den när den inte längre behövs.

Q3. Vad är lambda bra för?
Ans.
Svaret är:

  • Vi behöver ingen lambda, vi skulle klara oss bra utan den. Men…
  • det finns vissa situationer där det är bekvämt - det gör att skriva kod lite lättare och den skrivna koden lite renare.

Q4. Vilken typ av situationer?

Tja, situationer där vi behöver en enkel engångsfunktion: en funktion som bara kommer att användas en gång.

Normalt skapas funktioner för ett av två syften: (a) för att minska kodduplicering, eller (b) för att modularisera kod.

  • Om din applikation innehåller dubbletter av kod på olika ställen, kan du lägga en kopia av den koden i en funktion, ge funktionen ett namn och sedan - med hjälp av det funktionsnamnet - anropa den från olika ställen i din kod.
  • Om du har en bit kod som utför en väldefinierad operation - men är riktigt lång och knotrig och avbryter det annars läsbara flödet av ditt program - då kan du dra ut den där långa knotiga koden och lägga in den i en funktion helt själv.

Men anta att du behöver skapa en funktion som bara kommer att användas en gång - anropad från bara en plats i din ansökan. Tja, först och främst behöver du inte ge funktionen ett namn. Det kan vara "anonymt". Och du kan bara definiera den precis på den plats där du vill använda den. Det är där lambda är användbart.

Vanligtvis används lambda i samband med någon annan operation, såsom sortering eller en datareduktion:

namn = [ "David Beazley", "Brian Jones", "Raymond Hettinger", "Ned Batchelder" ]

print sorterade (namn, nyckel = lambda namn: namn. split () [-1]. lägre ())

# ["Ned Batchelder", "David Beazley", "Raymond Hettinger", "Brian Jones"]

Även om lambda låter dig definiera en enkel funktion, är dess användning mycket begränsad. I
i synnerhet kan endast ett enda uttryck specificeras, vars resultat är avkastningen
värde. Detta innebär att inga andra språkfunktioner, inklusive flera uttalanden, villkor, iteration och undantagshantering, kan inkluderas.
Du kan ganska glatt skriva mycket Python-kod utan att någonsin använda lambda. Dock,
du kommer ibland att stöta på det i program där någon skriver mycket små
funktioner som utvärderar olika uttryck, eller i program som kräver att användarna tillhandahåller
återuppringningsfunktioner.

Du har definierat en anonym funktion med lambda, men du måste också fånga
värden för vissa variabler vid definitionstillfället.

>>> x = 10

>>> a = lambda y : x + y

>>> x = 20

>>> b = lambda y : x + y

Ställ nu en fråga till dig själv. Vilka är värdena för a(10) och b(10)? Om du tror att
resultaten kan vara 20 och 30, du skulle ha fel:

Problemet här är att värdet på x som används i lambda-uttrycket är en fri variabel
som binds vid körning, inte definitionstid. Alltså värdet av x i lambda
uttryck är vad värdet på x-variabeln än råkar vara vid tidpunkten för exekvering.
Till exempel:

Om du vill ha en anonym funktion för att fånga ett värde vid definitionspunkten och
behåll det, inkludera värdet som ett standardvärde, så här:

Problemet som tas upp här är något som tenderar att komma upp i kod som
försöker vara lite för smart med användningen av lambdafunktioner. Till exempel,
skapa en lista med lambda-uttryck med hjälp av en listförståelse eller i en slinga av något slag och förväntar sig att lambda-funktionerna kommer ihåg iterationsvariabeln vid definitionstillfället. Till exempel:

>>> funcs = [ lambda x : x + n för n i intervallet (5 ) ]