Skip to content

Allt om WordPress, webbutveckling — och mer därtill

🐍 Funktionell programmering i Python

🐍 Funktionell programmering i Python

Varje Python-utvecklare får förr eller senare höra: "testa att skriva i funktionell stil, så blir din kod renare." Men när du öppnar dokumentationen ser du map, filter och reduce med lambdafunktioner och förstår inte var du ska börja.

Problemet är inte att funktionell programmering är svårt. Problemet är att de flesta guider antingen dyker ner i akademiska Haskell-snår eller begränsar sig till syntax utan att förklara "varför". Python, som är ett multiparadigmatiskt språk, ger dig precis så många funktionella verktyg som du behöver för vardagliga uppgifter, utan ytterligheter.

Här får du en praktisk genomgång av Pythons funktionella möjligheter: från listomfattningar till lambdafunktioner, med verkliga exempel och scenarier där varje verktyg på riktigt förenklar koden.

💡 Snabb översikt:

  • Bryter ner listomfattningar, varför de är bättre än loopar och när du ska använda dem
  • Bemästra generatorer: yield, minnesbesparingar och generatoruttryck
  • Går igenom map + filter + reduce: praktiska exempel på att ersätta loopar
  • Lär dig skriva lambdafunktioner och förstå var de passar och var de skadar läsbarheten

Funktionell programmering, vad pratar vi om

Funktionell programmering är ett angreppssätt där ett program byggs av matematiska funktioner: de tar oföränderlig indata och returnerar ett resultat utan att ändra något utanför. Inga sidoeffekter, ingen modifiering av globalt tillstånd.

I rent funktionella språk som Haskell är "rena" och "orena" (interagerar med omvärlden) delar av programmet strikt separerade. Detta angreppssätt möjliggör formella bevis för kodens korrekthet: kompilatorn garanterar frånvaron av oväntade sidoeffekter.

Python designades inte som ett funktionellt språk. Det är multiparadigmatiskt: objektorienterat, procedurellt och ja, funktionellt också. Att skriva i rent funktionell stil i Python är som att slå i spik med ett mikroskop: tekniskt möjligt, men obekvämt och onaturligt. Däremot är det ett fungerande och användbart mönster att plocka enskilda funktionella verktyg och bädda in dem i välbekant kod.

Listomfattningar, Pythons främsta funktionella verktyg

Det dagliga brödet inom funktionell programmering är att arbeta med listor. Välj element efter villkor, transformera varje element, bygg en ny lista från en befintlig, allt detta täcks i Python av listomfattningar.

Före, procedurell loop:

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]))

Efter, listomfattning:

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]

Fyra rader procedurell kod komprimerade till en. Läsbarheten blev inte lidande: syntaxen [expression for element in iterator if condition] är intuitivt tydlig, "ta x från li om x är udda."

Och detta är inte bara syntaktiskt socker. Listomfattning körs snabbare än en motsvarande loop med .append() eftersom den exekveras på C-nivå, inte på Python-tolkens nivå.

Vad är en iterator

Listomfattning bygger på konceptet iterator, ett objekt som returnerar nästa element i en sekvens när next() anropas. Alla objekt som implementerar metoden __iter__ kallas itererbara. Lista, sträng, tupel, dictionary, set, de är alla itererbara.

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

Här returnerar iter() en strängiterator, och listomfattningen itererar genom den och applicerar .upper() på varje tecken.

Generatorer, lata sekvenser

En iterator kan skapas inte bara från en befintlig samling, utan också med hjälp av en generatorfunktion. Istället för return använder den yield: funktionen "somnar", kommer ihåg sitt tillstånd, och vid nästa next()-anrop fortsätter den från samma ställe.

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))

Denna kod kommer att skriva ut siffror från 1 till 12. Funktionen gen skapar inte en lista med tusen element i minnet, den genererar värden ett och ett, på begäran. För sekvenser med miljontals poster (loggfilsrader, databasström) är skillnaden i minnesförbrukning flera storleksordningar.

Generatoruttryck

Kompakt syntax: samma som listomfattningar, men med parenteser istället för hakparenteser.

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

Detta är inte en tupel, detta är en generator. Regeln är enkel: hakparenteser → lista (ivrig, allt i minnet), parenteser → generator (lat, ett element i taget).

Generatorer är en brygga mellan procedurell och funktionell stil i Python: de erbjuder lat evaluering utan att dyka ner i monadteori.

Map, filter och reduce: tre pelare för funktionell bearbetning

Tre inbyggda funktioner som i funktionella språk är grunden för allt. Python implementerar dem på sitt eget sätt, och det är viktigt att känna till nyanserna i de Pythonspecifika versionerna.

Map, applicera en funktion på varje element

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

map tar en funktion och en sekvens, och returnerar en iterator med resultaten av att applicera funktionen på varje element. I Python 3 returnerar map en iterator, inte en lista, så du behöver list() för att se resultatet.

För enkla transformationer är listomfattning ofta mer lättläst:

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

Filter, välj element efter villkor

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

filter behåller endast de element för vilka predikatfunktionen returnerar True. Precis som map returnerar den en iterator i Python 3.

Reduce, vik ihop en sekvens till ett enda värde

Till skillnad från map och filter lever reduce inte i det inbyggda scopet, utan i modulen functools. Den applicerar sekventiellt en funktion på element och ackumulerar resultatet.

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

Under huven: ((((1 + 2) + 3) + 4) + 5) = 15. För att summera tal i Python finns den inbyggda sum(), så reduce används oftare för icke-standardiserade vikningar: bygga ett träd från en platt lista, slå samman nästlade dictionaries, beräkna den största gemensamma delaren för en sekvens.

Lambdafunktioner, anonyma hjälpredor

När en funktion behövs exakt en gång och dess kropp ryms i ett enda uttryck, använd lambda:

1lambda arguments: expression

Samma filter-exempel kan skrivas om utan en separat def:

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

Lambda är en kompromiss. Plus: inget behov av att deklarera en separat funktion för en trivial kontroll. Minus: läsbarheten sjunker om uttrycket blir mer komplext än ett par operationer. Tumregel: om lambdafunktionen inte ryms på en rad eller du börjar nästla lambdafunktioner inuti varandra, bryt ut en vanlig def.

Var lambdafunktioner faktiskt är användbara

Det vanligaste scenariot är sortering efter en icke-standardnyckel:

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

Eller snabb transformation i map/filter när kroppen verkligen är trivial:

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

Men om du sträcker dig efter en lambda för logik med fyra eller fem operationer, stopp. Ett funktionsnamn fungerar som dokumentation, och def ger utrymme för en docstring.

⁉️🤔 Vanliga frågor

När är en listomfattning bättre än map?

En listomfattning som [x*2 for x in data] läses mer naturligt än list(map(lambda x: x*2, data)). En inbyggd operation med en lambda har två nivåer av indirektion, medan en listomfattning har en. Men om funktionen redan finns som en separat def är map med den koncis. Listomfattning vinner i läsbarhet för enkla transformationer, särskilt med filtrering genom [... if ...]. För komplex logik med mellanliggande variabler är en vanlig for-loop oslagbar.

Generator eller lista, vilken ska man välja?

Om resultatet behövs flera gånger (iteration, indexering, längd), ta en lista. Om du itererar en gång och datamängden är stor, kommer en generator att spara minne. En lista med en miljon heltal tar cirka 8 MB, en generator för samma intervall tar mindre än en kilobyte. Men en generator kan inte "spolas tillbaka" eller nås via index. Praktisk regel: data från en fil eller databas, generator; resultat för upprepad användning, list().

Skadar lambdafunktioner kodens läsbarhet?

I små doser och för trivial logik, nej. key=lambda x: x["price"] läses omedelbart. Men så fort en lambda slutar vara "en tanke", bryt ut den till en vanlig funktion. Team med kodgranskning begränsar lambdafunktioner till ett uttryck utan nästling. Om en kollega behöver mer än fem sekunder för att förstå den, skriv om den som en def. Namnet fungerar som dokumentation: def by_price(item): return item["price"] är tydligare än en anonym lambda.

Hur mycket långsammare är funktionell Python än procedurell?

För de flesta vardagliga uppgifter är skillnaden försumbar, bråkdelar av en mikrosekund. Listomfattning är ofta snabbare än en motsvarande loop eftersom den är implementerad i C. Flaskhalsen i Python-program är inte valet mellan map och en loop, utan input/output, nätverksanrop och arkitektur i allmänhet. Skriv tydligt, profilera hotspots med cProfile och optimera bara det som faktiskt saktar ner.

Vad du kan använda i din kod idag

Pythons funktionella verktyg är inte dogmer, utan hävstänger. De ersätter inte procedurell och objektorienterad kod, utan kompletterar den på specifika punkter.

Börja med listomfattningar, detta är den vanligaste och säkraste tillämpningen. Bemästra generatorer: så fort du ser en temporär lista som används en gång, fråga dig själv, borde inte detta ersättas med yield? Använd map och filter där de på riktigt förenklar en transformationskedja. Spara lambdafunktioner till key= i sortering och triviala callbacks.

Funktionell stil i Python handlar inte om "renhet", utan om uttrycksfullhet. Om koden blev tydligare efter att du ersatte en loop med en listomfattning, gjorde du allt rätt. Testa att skriva om en problematisk metod i funktionell stil och jämför diffen: ofta talar resultatet för sig självt.