Python: Декларативное программирование
Теория: Генераторы списков
В повседневной жизни разработчика часто встречается код, работающий с последовательностями. Это связано с тем, что итераторы встроены в Python и тесно интегрированы в стандартную библиотеку.
Итераторы и операции над ними обычно собираются в конвейеры для данных. Лишь в конце каждого конвейера стоит reduce() или другой потребитель элементов, не передающий элементы дальше.
Большинство таких конвейеров состоит из двух видов операций:
- Преобразование отдельных элементов. Эту задачу выполняет функция
map(). Она преобразует весь поток с помощью другой функции, обрабатывающей отдельные элементы - Изменение состава элементов, то есть фильтрация или размножение. Фильтровать данные умеет
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 считается императивным из-за его тела, а не из-за заголовка.
Завершено
0 / 6