
🐍 Функціональне програмування на Python
Будь-який Python-розробник рано чи пізно чує: «спробуй писати у функціональному стилі, код стане чистішим». Але коли відкриваєш документацію, бачиш map, filter і reduce з лямбдами й не розумієш, з якого боку підступитися.
Проблема не в тому, що функціональне програмування складне. Проблема в тому, що більшість посібників або йдуть в академічні нетрі Haskell, або обмежуються синтаксисом без пояснення «навіщо». А Python, будучи мультипарадигмальною мовою, дає рівно стільки функціональних інструментів, скільки потрібно для повсякденних задач, без крайнощів.
У цьому матеріалі, прикладний розбір функціональних можливостей Python: від list comprehensions до лямбда-функцій, із живими прикладами та сценаріями, де кожен інструмент справді спрощує код.
💡 Швидкий огляд:
- Розбираємо list comprehensions, чим вони кращі за цикли та коли їх використовувати
- Освоюємо генератори:
yield, економія пам’яті та вирази-генератори - Проходимо зв’язку
map+filter+reduce: практичні приклади заміни циклів - Вчимося писати лямбда-функції та розуміємо, де вони доречні, а де шкодять читаності
Функціональне програмування, про що мова
Функціональне програмування — це підхід, за якого програма будується з математичних функцій: вони приймають незмінні вхідні дані та повертають результат, не змінюючи нічого назовні. Жодних побічних ефектів, жодної зміни глобального стану.
У чистих функціональних мовах, як-от Haskell, «чисті» та «нечисті» (ті, що взаємодіють із зовнішнім світом) частини програми жорстко розділені. Такий підхід дає змогу формально доводити коректність коду: компілятор гарантує відсутність неочікуваних сайд-ефектів.
Python не проєктувався як функціональна мова. Він мультипарадигмальний: об’єктно-орієнтований, процедурний, і так, функціональний теж. Писати на Python у суто функціональному стилі — це як забивати цвяхи мікроскопом: технічно можливо, але незручно та протиприродно. Однак узяти окремі функціональні інструменти та вбудувати їх у звичний код, робочий і корисний патерн.
List comprehensions, головний функціональний інструмент Python
Хліб функціонального програмування, робота зі списками. Вибрати елементи за умовою, перетворити кожен елемент, побудувати новий список із наявного, усе це в Python покривається list comprehensions (списковими включеннями).
Було, процедурний цикл:
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]))
Стало, 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]
Чотири рядки процедурного коду стиснулися в один. Читаність не постраждала: синтаксис [выражение for элемент in итератор if условие] інтуїтивно зрозумілий, «візьми x з li, якщо x непарний».
І це не просто синтаксичний цукор. List comprehension працює швидше за еквівалентний цикл із .append(), тому що виконується на рівні C, а не Python-інтерпретатора.
Що таке ітератор
List comprehension спирається на концепцію ітератора, об’єкта, який за запитом next() віддає наступний елемент послідовності. Будь-який об’єкт, що реалізує метод __iter__, називається ітерованим. Список, рядок, кортеж, словник, множина, усі вони ітеровані.
1 spam_iter = iter("foobar") 2 result = "".join([c.upper() for c in spam_iter]) 3 print(result) # FOOBAR
Тут iter() повертає ітератор рядка, а list comprehension проходить по ньому, застосовуючи .upper() до кожного символу.
Генератори, ледачі послідовності
Ітератор можна створити не лише з готової колекції, а й за допомогою функції-генератора. Замість return вона використовує yield: функція «засинає», запам’ятовуючи стан, і при наступному виклику next() продовжує з того самого місця.
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))
Цей код надрукує числа від 1 до 12. Функція gen не створює список із тисячі елементів у пам’яті, вона генерує значення по одному, на вимогу. Для послідовностей із мільйонів записів (рядки лог-файлу, потік із БД) різниця у споживанні пам’яті, на порядки.
Вирази-генератори
Компактний синтаксис: ті самі list comprehensions, але в круглих дужках замість квадратних.
1 g = (c.upper() for c in "foobar")
Це не кортеж — це генератор. Правило просте: квадратні дужки → список (жадібний, весь у пам’яті), круглі дужки → генератор (ледачий, по одному елементу).
Генератори, міст між процедурним і функціональним стилем у Python: вони дають ледачі обчислення без занурення в теорію монад.
Map, filter і reduce: три кити функціональної обробки
Три вбудовані функції, які у функціональних мовах є основою всього. Python реалізує їх по-своєму, і важливо знати нюанси саме Python-версій.
Map, застосувати функцію до кожного елемента
1 def square(x): 2 return x * x 3 4 result = map(square, [1, 2, 3]) 5 print(list(result)) # [1, 4, 9]
map бере функцію та послідовність і повертає ітератор із результатами застосування функції до кожного елемента. У Python 3 map повертає саме ітератор, а не список, щоб побачити результат, потрібен list().
Для простих перетворень list comprehension часто читається краще:
1 [x * x for x in [1, 2, 3]] # то же самое, но привычнее глазу
Filter, вибрати елементи за умовою
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 залишає лише ті елементи, для яких функція-предикат повертає True. Як і map, повертає ітератор у Python 3.
Reduce, згорнути послідовність в одне значення
На відміну від map і filter, reduce живе не у вбудованій області видимості, а в модулі functools. Він послідовно застосовує функцію до елементів, накопичуючи результат.
1 from functools import reduce 2 3 def add(a, b): 4 return a + b 5 6 print(reduce(add, range(1, 6))) # 15
Під капотом: ((((1 + 2) + 3) + 4) + 5) = 15. Для підсумовування чисел у Python є вбудована sum(), тож reduce частіше застосовують для нестандартних згорток: побудувати дерево з плаского списку, об’єднати вкладені словники, обчислити найбільший спільний дільник послідовності.
Лямбда-функції, анонімні помічники
Коли функція потрібна рівно на один раз і її тіло вміщується в один вираз, використовують lambda:
1 lambda аргументы: выражение
Той самий приклад із filter можна переписати без окремої def:
1 result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))
Лямбда — це компроміс. Плюс: не потрібно оголошувати окрему функцію заради однієї тривіальної перевірки. Мінус: читаність падає, якщо вираз стає складнішим за пару операцій. Правило великого пальця: якщо лямбда не вміщується в один рядок або ви починаєте вкладати лямбди одна в одну, виносьте звичайну def.
Де лямбди справді корисні
Найчастіший сценарій, сортування за нестандартним ключем:
1 users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}] 2 users.sort(key=lambda u: u["age"])
Або швидка трансформація в map/filter, коли тіло справді тривіальне:
1 squares = list(map(lambda x: x * x, range(10)))
Але якщо ви тягнетеся до лямбди для логіки на чотири-п’ять операцій, зупиніться. Назва функції працює як документація, а def дає місце для docstring-а.
⁉️🤔 Часті питання
Коли list comprehension кращий, ніж map?
List comprehension на кшталт
[x*2 for x in data]читається природніше, ніжlist(map(lambda x: x*2, data)). Вбудована операція з лямбдою, два рівні опосередкованості, а спискове включення, один. Але якщо функція вже існує як окремаdef,mapіз нею лаконічний. List comprehension виграє в читаності для простих трансформацій, особливо з фільтрацією через[... if ...]. Для складної логіки з проміжними змінними звичайний циклforпоза конкуренцією.
Генератор чи список, що вибрати?
Якщо результат потрібен багаторазово (перебір, індексація, довжина), беріть список. Якщо проходите один раз і обсяг даних великий, генератор зекономить пам’ять. Список на мільйон цілих чисел займає близько 8 МБ, генератор на той самий діапазон, менше кілобайта. Але генератор не можна «перемотати» чи взяти за індексом. Практичне правило: дані з файлу чи БД, генератор; результат для повторного використання,
list().
Чи шкодять лямбди читаності коду?
У малих дозах і для тривіальної логіки, ні.
key=lambda x: x["price"]читається миттєво. Але щойно лямбда перестає бути «однією думкою», виносьте у звичайну функцію. Команди з code review обмежують лямбди одним виразом без вкладень. Якщо колезі потрібно більше п’яти секунд на розуміння, перепишіть наdef. Назва слугує документацією:def by_price(item): return item["price"]зрозуміліша за безіменнуlambda.
Наскільки функціональний Python повільніший за процедурний?
Для більшості повсякденних задач різниця непомітна, долі мікросекунди. List comprehension часто швидший за еквівалентний цикл, тому що реалізований на C. Вузьке місце Python-програм, не вибір між
mapі циклом, а введення-виведення, мережеві запити й архітектура загалом. Пишіть зрозуміло, профілюйте гарячі ділянки черезcProfileта оптимізуйте лише те, що реально гальмує.
Що взяти у свій код уже сьогодні
Функціональні інструменти Python, не догма, а важелі. Вони не замінюють процедурний і об’єктно-орієнтований код, а доповнюють його в конкретних точках.
Почніть із list comprehensions — це найчастіше й безпрограшне застосування. Освойте генератори: щойно бачите тимчасовий список, який використовується один раз, запитайте себе, а чи не замінити його на yield? map і filter підключайте там, де вони справді спрощують ланцюжок перетворень. Лямбди залиште для key= у сортуваннях і тривіальних колбеків.
Функціональний стиль у Python — це не про «чистоту», а про виразність. Якщо після заміни циклу на list comprehension код став зрозумілішим, ви все зробили правильно. Спробуйте переписати один проблемний метод у функціональному ключі та порівняйте diff: часто результат говорить сам за себе.



