Skip to content

Wszystko o WordPressie, tworzeniu stron — i nie tylko

🐍 Programowanie funkcyjne w Pythonie

🐍 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:

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

Jest, list comprehension:

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]

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.

1spam_iter = iter("foobar")
2result = "".join([c.upper() for c in spam_iter])
3print(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.

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

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.

1g = (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

1def square(x):
2 return x * x
3
4result = map(square, [1, 2, 3])
5print(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

1def is_upper(c):
2 return c == c.upper()
3
4result = filter(is_upper, "FreedominObscureandoutlandishcOde")
5print(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.

1from functools import reduce
2
3def add(a, b):
4 return a + b
5
6print(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:

1lambda аргументы: выражение

Ten sam przykład z filter można przepisać bez osobnej def:

1result = 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:

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

Albo szybka transformacja w map/filter, gdy ciało jest naprawdę trywialne:

1squares = 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 osobna def, map z 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ętla for jest 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ć na def. Nazwa służy jako dokumentacja: def by_price(item): return item["price"] jest zrozumialsza niż bezimienna lambda.

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 map a pętlą, lecz operacje wejścia-wyjścia, zapytania sieciowe i ogólna architektura. Proszę pisać zrozumiale, profilować gorące fragmenty przez cProfile i 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.