Python: Декларативное программирование
2026-02-26 20:35 Diff

В повседневной жизни разработчика часто встречается код, работающий с последовательностями. Это связано с тем, что итераторы встроены в Python и тесно интегрированы в стандартную библиотеку.

Итераторы и операции над ними обычно собираются в конвейеры для данных. Лишь в конце каждого конвейера стоит reduce() или другой потребитель элементов, не передающий элементы дальше.

Большинство таких конвейеров состоит из двух видов операций:

  1. Преобразование отдельных элементов. Эту задачу выполняет функция map(). Она преобразует весь поток с помощью другой функции, обрабатывающей отдельные элементы
  2. Изменение состава элементов, то есть фильтрация или размножение. Фильтровать данные умеет filter(). А уже map() в паре с chain() из модуля itertools превращают каждый элемент в несколько, не меняя при этом уровень вложенности

Для примера представим, что мы хотим получить список чисел вида [0, 0, 2, 2, 4, 4...] — то есть по две копии возрастающих четных чисел. Напишем подходящий конвейер:

Как видите, задача решается соединением готовых элементов, а не написанием всего кода вручную в виде цикла for. Уже здесь виден минус нашего конструктора: если готовых функций над элементами или предикатов нет, то их либо приходится заранее объявлять, либо использовать lambda.

Оба варианта неудобны. Когда другой человек читает наш код с отдельными функциями, ему приходится постоянно прыгать по коду туда-сюда. А lambda просто смотрятся громоздко. Но отчаиваться не нужно: у Python есть синтаксис, который может упростить работу с конвейерами.

Генераторы списков

Попробуем решить ту же задачу другим способом:

Это тоже однострочник. Выглядит он не очень удобно, но к такому синтаксису можно привыкнуть. Попробуем отформатировать все выражение:

Теперь код стал похож на два вложенных цикла. Похожий код можно написать и на обычных циклах:

Код выглядит очень похоже, но есть два различия:

  • В первом варианте мы создаем новый список, а во втором — изменяем заранее созданный
  • Первый вариант — это выражение, а второй — набор инструкций. Следовательно, первый вариант можно использовать как часть любых других выражений. При этом нам не пришлось объявлять вспомогательные функции, лямбды тоже не понадобились

Выражения вида [… for … in …] называются генераторами списков. Рассмотрим составляющие нового синтаксиса.

Генератор списков описывается так:

Рассмотрим этот шаблон подробнее:

  • ВЫРАЖЕНИЕ может использовать ПЕРЕМЕННУЮ и вычисляется в элемент будущего списка
  • ПЕРЕМЕННАЯ — имя, с которым поочередно связываются элементы ИСТОЧНИКА
  • ИСТОЧНИК — любой итератор или итерируемый объект
  • УСЛОВИЕ — выражение, которое использует ПЕРЕМЕННУЮ, вычисляемую на каждой итерации

Если условие оказывается ложным, то вычисление выражения для текущей итерации пропускается — в итоговый список новый элемент не добавится. Если условие вместе с ключевым словом if будет пропущено, то это будет эквивалентно условию if True.

В общем случае переменных может быть несколько. Здесь тоже работает распаковка кортежей и списков, в том числе и вложенных.

Вот несколько примеров:

Когда использовать генераторы списков

Выше мы увидели, что генераторы списков не отменяют все встроенные функции для работы с итераторами. Одно с другим отлично сочетается.

С другой стороны, лучше не смешивать генераторы списков с функциями map() и filter() — это как раз взаимозаменяемые сущности. Еще не стоит смешивать генераторы списков с какими-либо побочными эффектами. Дело в том, что генераторы позволяют писать довольно лаконичный и компактный код. Не нужно заставлять программиста думать, где и что поменяется при создании списка.

Это касается не только кода с функциями map() и filter(), но и вообще любых декларативных конвейеров. Стоит разделять код, написанный в разных парадигмах, на отдельные однотонные участки. Например, ввод-вывод — это один из основных видов побочных эффектов. Он может находиться в начале конвейера или в его конце, но не в середине.

Как проявляется декларативность генераторов списков

Разберем, чем генератор списка отличается от явно императивного двойного цикла. В цикле можно не только строить список, но и производить другие побочные эффекты — например, изменять объекты списка.

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

Можно взглянуть на отличающиеся части и увидеть, что:

  • Генератор списка описывает результат. Он говорит: «Результирующий список — это список чисел в диапазоне от 1 до 20
  • Процедурное решение показывает, как получить результат. Оно говорит: «Для каждого числа в диапазоне до 20 добавляем в список число»

Сами циклы for в обоих случаях выглядят одинаково, потому что в Python циклы более декларативные, чем в некоторых других языках. Таким образом, цикл for в Python считается императивным из-за его тела, а не из-за заголовка.