
🐍 Programowanie funkcyjne w Pythonie
Każdy programista Pythona prędzej czy później słyszy: „spróbuj pisać w stylu funkcyjnym, kod stanie się czystszy". Ale kiedy otwiera Pan dokumentację, widzi map, filter i reduce z lambdami i nie rozumie, od czego zacząć.
Problem nie w tym, że programowanie funkcyjne jest trudne. Problem w tym, że większość poradników albo zagłębia się w akademickie zawiłości Haskella, albo ogranicza się do składni bez wyjaśnienia „po co". A Python, jako język wieloparadygmatowy, daje dokładnie tyle narzędzi funkcyjnych, ile potrzeba do codziennych zadań, bez popadania w skrajności.
W tym materiale: praktyczna analiza funkcyjnych możliwości Pythona, od list comprehensions po funkcje lambda, z żywymi przykładami i scenariuszami, w których każde narzędzie naprawdę upraszcza kod.
💡 Szybki przegląd:
- Analizujemy list comprehensions, dlaczego są lepsze od pętli i kiedy ich używać
- Opanowujemy generatory:
yield, oszczędność pamięci i wyrażenia generatorowe - Przerabiamy zestaw
map+filter+reduce: praktyczne przykłady zastępowania pętli - Uczymy się pisać funkcje lambda i rozumiemy, gdzie są odpowiednie, a gdzie szkodzą czytelności
Programowanie funkcyjne, o co chodzi
Programowanie funkcyjne to podejście, w którym program buduje się z funkcji matematycznych: przyjmują one niezmienne dane wejściowe i zwracają wynik, nie zmieniając niczego na zewnątrz. Żadnych efektów ubocznych, żadnej zmiany stanu globalnego.
W czystych językach funkcyjnych, takich jak Haskell, „czyste" i „nieczyste" (te, które wchodzą w interakcję ze światem zewnętrznym) części programu są ściśle rozdzielone. Takie podejście umożliwia formalne dowodzenie poprawności kodu: kompilator gwarantuje brak nieoczekiwanych efektów ubocznych.
Python nie był projektowany jako język funkcyjny. Jest wieloparadygmatowy: obiektowy, proceduralny i owszem, funkcyjny również. Pisanie w Pythonie w czysto funkcyjnym stylu to jak wbijanie gwoździ mikroskopem: technicznie możliwe, ale niewygodne i nienaturalne. Natomiast wzięcie pojedynczych narzędzi funkcyjnych i wbudowanie ich w zwykły kod to działający i użyteczny wzorzec.
List comprehensions, główne narzędzie funkcyjne Pythona
Chleb powszedni programowania funkcyjnego to praca z listami. Wybrać elementy według warunku, przekształcić każdy element, zbudować nową listę z istniejącej, wszystko to w Pythonie realizują list comprehensions (wyrażenia listowe).
Było, pętla proceduralna:
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]))
Jest, list comprehension:
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]
Cztery linie kodu proceduralnego skurczyły się do jednej. Czytelność nie ucierpiała: składnia [выражение for элемент in итератор if условие] jest intuicyjnie zrozumiała, „weź x z li, jeśli x jest nieparzyste".
I to nie jest tylko lukier składniowy. List comprehension działa szybciej niż równoważna pętla z .append(), ponieważ jest wykonywane na poziomie C, a nie interpretera Pythona.
Czym jest iterator
List comprehension opiera się na koncepcji iteratora, obiektu, który na żądanie next() zwraca kolejny element sekwencji. Każdy obiekt implementujący metodę __iter__ nazywany jest iterowalnym. Lista, łańcuch znaków, krotka, słownik, zbiór, wszystkie są iterowalne.
1 spam_iter = iter("foobar") 2 result = "".join([c.upper() for c in spam_iter]) 3 print(result) # FOOBAR
Tutaj iter() zwraca iterator łańcucha, a list comprehension przechodzi po nim, stosując .upper() do każdego znaku.
Generatory, leniwe sekwencje
Iterator można utworzyć nie tylko z gotowej kolekcji, ale także za pomocą funkcji generatora. Zamiast return używa ona yield: funkcja „zasypia", zapamiętując stan, a przy kolejnym wywołaniu next() kontynuuje od tego samego miejsca.
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))
Ten kod wypisze liczby od 1 do 12. Funkcja gen nie tworzy w pamięci listy tysiąca elementów, generuje wartości pojedynczo, na żądanie. Dla sekwencji z milionów rekordów (linie pliku logu, strumień z bazy danych) różnica w zużyciu pamięci jest rzędu wielkości.
Wyrażenia generatorowe
Kompaktowa składnia: te same list comprehensions, ale w nawiasach okrągłych zamiast kwadratowych.
1 g = (c.upper() for c in "foobar")
To nie jest krotka, to generator. Zasada jest prosta: nawiasy kwadratowe → lista (zachłanna, cała w pamięci), nawiasy okrągłe → generator (leniwy, po jednym elemencie).
Generatory to pomost między stylem proceduralnym a funkcyjnym w Pythonie: dają leniwe obliczenia bez zagłębiania się w teorię monad.
Map, filter i reduce: trzy filary przetwarzania funkcyjnego
Trzy wbudowane funkcje, które w językach funkcyjnych są podstawą wszystkiego. Python realizuje je po swojemu i ważne jest, aby znać niuanse właśnie wersji pythonowych.
Map, zastosuj funkcję do każdego elementu
1 def square(x): 2 return x * x 3 4 result = map(square, [1, 2, 3]) 5 print(list(result)) # [1, 4, 9]
map bierze funkcję i sekwencję, po czym zwraca iterator z wynikami zastosowania funkcji do każdego elementu. W Pythonie 3 map zwraca właśnie iterator, a nie listę, aby zobaczyć wynik, potrzebny jest list().
Dla prostych przekształceń list comprehension często czyta się lepiej:
1 [x * x for x in [1, 2, 3]] # то же самое, но привычнее глазу
Filter, wybierz elementy według warunku
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 pozostawia tylko te elementy, dla których funkcja predykat zwraca True. Podobnie jak map, zwraca iterator w Pythonie 3.
Reduce, zwiń sekwencję do jednej wartości
W przeciwieństwie do map i filter, reduce nie znajduje się we wbudowanym zakresie, lecz w module functools. Stosuje on kolejno funkcję do elementów, akumulując wynik.
1 from functools import reduce 2 3 def add(a, b): 4 return a + b 5 6 print(reduce(add, range(1, 6))) # 15
Pod maską: ((((1 + 2) + 3) + 4) + 5) = 15. Do sumowania liczb w Pythonie istnieje wbudowana funkcja sum(), dlatego reduce częściej stosuje się do niestandardowych zwinięć: zbudowania drzewa z płaskiej listy, scalenia zagnieżdżonych słowników, obliczenia największego wspólnego dzielnika sekwencji.
Funkcje lambda, anonimowi pomocnicy
Kiedy funkcja jest potrzebna dokładnie na jeden raz, a jej ciało mieści się w jednym wyrażeniu, używa się lambda:
1 lambda аргументы: выражение
Ten sam przykład z filter można przepisać bez osobnej def:
1 result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))
Lambda to kompromis. Plus: nie trzeba deklarować osobnej funkcji dla jednego trywialnego sprawdzenia. Minus: czytelność spada, jeśli wyrażenie staje się bardziej złożone niż kilka operacji. Praktyczna zasada: jeśli lambda nie mieści się w jednej linii albo zaczyna Pan zagnieżdżać lambdy jedna w drugiej, proszę wydzielić zwykłą def.
Gdzie lambdy są naprawdę przydatne
Najczęstszy scenariusz to sortowanie według niestandardowego klucza:
1 users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}] 2 users.sort(key=lambda u: u["age"])
Albo szybka transformacja w map/filter, gdy ciało jest naprawdę trywialne:
1 squares = list(map(lambda x: x * x, range(10)))
Ale jeśli sięga Pan po lambdę do logiki na cztery, pięć operacji, proszę się zatrzymać. Nazwa funkcji działa jak dokumentacja, a def daje miejsce na docstring.
⁉️🤔 Częste pytania
Kiedy list comprehension jest lepsze niż map?
List comprehension w rodzaju
[x*2 for x in data]czyta się naturalniej niżlist(map(lambda x: x*2, data)). Wbudowana operacja z lambdą to dwa poziomy pośredniości, a wyrażenie listowe to jeden. Ale jeśli funkcja już istnieje jako osobnadef,mapz nią jest lakoniczny. List comprehension wygrywa czytelnością dla prostych transformacji, szczególnie z filtrowaniem przez[... if ...]. Dla złożonej logiki ze zmiennymi pośrednimi zwykła pętlaforjest bezkonkurencyjna.
Generator czy lista, co wybrać?
Jeśli wynik jest potrzebny wielokrotnie (iterowanie, indeksowanie, długość), proszę wziąć listę. Jeśli przechodzi Pan raz, a objętość danych jest duża, generator zaoszczędzi pamięć. Lista miliona liczb całkowitych zajmuje około 8 MB, generator dla tego samego zakresu to mniej niż kilobajt. Ale generatora nie można „przewinąć" ani pobrać elementu po indeksie. Praktyczna reguła: dane z pliku lub bazy danych to generator; wynik do ponownego użycia to
list().
Czy lambdy szkodzą czytelności kodu?
W małych dawkach i dla trywialnej logiki nie.
key=lambda x: x["price"]czyta się natychmiast. Ale gdy tylko lambda przestaje być „jedną myślą", proszę wydzielić do zwykłej funkcji. Zespoły podczas code review ograniczają lambdy do jednego wyrażenia bez zagnieżdżeń. Jeśli koledze potrzeba więcej niż pięciu sekund na zrozumienie, proszę przepisać nadef. Nazwa służy jako dokumentacja:def by_price(item): return item["price"]jest zrozumialsza niż bezimiennalambda.
O ile funkcyjny Python jest wolniejszy od proceduralnego?
Dla większości codziennych zadań różnica jest niezauważalna, to ułamki mikrosekund. List comprehension jest często szybsze niż równoważna pętla, ponieważ jest zaimplementowane w C. Wąskim gardłem programów w Pythonie nie jest wybór między
mapa pętlą, lecz operacje wejścia-wyjścia, zapytania sieciowe i ogólna architektura. Proszę pisać zrozumiale, profilować gorące fragmenty przezcProfilei optymalizować tylko to, co naprawdę spowalnia.
Co wziąć do swojego kodu już dziś
Narzędzia funkcyjne Pythona to nie dogmat, a dźwignie. Nie zastępują one kodu proceduralnego i obiektowego, lecz uzupełniają go w konkretnych punktach.
Proszę zacząć od list comprehensions, to najczęstsze i bezbłędne zastosowanie. Proszę opanować generatory: gdy tylko widzi Pan tymczasową listę, która jest używana jeden raz, proszę zadać sobie pytanie, czy nie zastąpić jej przez yield? map i filter proszę włączać tam, gdzie naprawdę upraszczają łańcuch przekształceń. Lambdy proszę zostawić dla key= w sortowaniach i trywialnych callbacków.
Styl funkcyjny w Pythonie nie polega na „czystości", lecz na wyrazistości. Jeśli po zastąpieniu pętli przez list comprehension kod stał się zrozumialszy, zrobił Pan wszystko prawidłowo. Proszę spróbować przepisać jedną problematyczną metodę w kluczu funkcyjnym i porównać diff: często wynik mówi sam za siebie.



