Skip to content

Всё для WordPress, веб-разработки — и не только

🐍 Функциональное программирование на Python

🐍 Функциональное программирование на 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 (списочными включениями).

Было, процедурный цикл:

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

Стало, 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]

Четыре строки процедурного кода сжались в одну. Читаемость не пострадала: синтаксис [выражение for элемент in итератор if условие] интуитивно понятен, «возьми x из li, если x нечётный».

И это не просто синтаксический сахар. List comprehension работает быстрее эквивалентного цикла с .append(), потому что выполняется на уровне C, а не Python-интерпретатора.

Что такое итератор

List comprehension опирается на концепцию итератора, объекта, который по запросу next() отдаёт следующий элемент последовательности. Любой объект, реализующий метод __iter__, называется итерируемым. Список, строка, кортеж, словарь, множество, все они итерируемы.

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

Здесь iter() возвращает итератор строки, а list comprehension проходит по нему, применяя .upper() к каждому символу.

Генераторы, ленивые последовательности

Итератор можно создать не только из готовой коллекции, но и с помощью функции-генератора. Вместо return она использует yield: функция «засыпает», запоминая состояние, и при следующем вызове next() продолжает с того же места.

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

Этот код напечатает числа от 1 до 12. Функция gen не создаёт список из тысячи элементов в памяти, она генерирует значения по одному, по требованию. Для последовательностей из миллионов записей (строки лог-файла, поток из БД) разница в потреблении памяти, на порядки.

Выражения-генераторы

Компактный синтаксис: те же list comprehensions, но в круглых скобках вместо квадратных.

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

Это не кортеж, это генератор. Правило простое: квадратные скобки → список (жадный, весь в памяти), круглые скобки → генератор (ленивый, по одному элементу).

Генераторы, мост между процедурным и функциональным стилем в Python: они дают ленивые вычисления без погружения в теорию монад.

map, filter и reduce: три кита функциональной обработки

Три встроенные функции, которые в функциональных языках являются основой всего. Python реализует их по-своему, и важно знать нюансы именно Python-версий.

map, применить функцию к каждому элементу

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

map берёт функцию и последовательность, и возвращает итератор с результатами применения функции к каждому элементу. В Python 3 map возвращает именно итератор, а не список, чтобы увидеть результат, нужен list().

Для простых преобразований list comprehension часто читается лучше:

1[x * x for x in [1, 2, 3]] # то же самое, но привычнее глазу

filter, выбрать элементы по условию

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

filter оставляет только те элементы, для которых функция-предикат возвращает True. Как и map, возвращает итератор в Python 3.

reduce, свернуть последовательность в одно значение

В отличие от map и filter, reduce живёт не во встроенной области видимости, а в модуле functools. Он последовательно применяет функцию к элементам, накапливая результат.

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

Под капотом: ((((1 + 2) + 3) + 4) + 5) = 15. Для суммирования чисел в Python есть встроенная sum(), так что reduce чаще применяют для нестандартных свёрток: построить дерево из плоского списка, объединить вложенные словари, вычислить наибольший общий делитель последовательности.

Лямбда-функции, анонимные помощники

Когда функция нужна ровно на один раз и её тело умещается в одно выражение, используют lambda:

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

Тот же пример с filter можно переписать без отдельной def:

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

Лямбда, это компромисс. Плюс: не нужно объявлять отдельную функцию ради одной тривиальной проверки. Минус: читаемость падает, если выражение становится сложнее пары операций. Правило большого пальца: если лямбда не помещается в одну строку или вы начинаете вкладывать лямбды друг в друга, выносите обычную def.

Где лямбды реально полезны

Самый частый сценарий, сортировка по нестандартному ключу:

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

Или быстрая трансформация в map/filter, когда тело действительно тривиально:

1squares = 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: часто результат говорит сам за себя.