
🐍 Функциональное программирование на 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: часто результат говорит сам за себя.



