Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Генераторы списков Python: Декларативное программирование

Конвейерная разработка

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

  1. Преобразование отдельных элементов
  2. Изменение состава элементов — фильтрация или, наоборот, размножение

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

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

# получаем поток четных чисел
def is_even(x):
    return x % 2 == 0

list(filter(is_even, range(20)))
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# удваиваем каждое
def dup(x):
    return [x, x]

list(map(dup, filter(is_even, range(20))))
# [[0, 0], [2, 2], [4, 4], [6, 6], [8, 8], [10, 10], [12, 12], [14, 14], [16, 16], [18, 18]]

# делаем конвеер опять плоским
from itertools import chain
list(chain(*map(dup, filter(is_even, range(20)))))
# [0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14, 16, 16, 18, 18]

# и вариант в виде однострочника
list(chain(*map(lambda x: [x, x], filter(lambda x: x % 2 == 0, range(20)))))
# [0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14, 16, 16, 18, 18]

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

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

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

Взглянем на пример решения описанной выше задачи альтернативным способом:

[x for num in range(20) for x in [num, num] if num % 2 == 0]
# [0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14, 16, 16, 18, 18]

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

[x
   for num in range(20)
       for x in [num, num]
           if num % 2 == 0
]
# [0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14, 16, 16, 18, 18]

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

res = []
for y in range(20):
    for x in [y, y]:
        if y % 2 == 0:
            res.append(x)

res
# [0, 0, 2, 2, 4, 4, 6, 6, 8, 8, 10, 10, 12, 12, 14, 14, 16, 16, 18, 18]

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

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

Анатомия генераторов списков

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

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

Если УСЛОВИЕ оказывается ложным, то вычисление ВЫРАЖЕНИЯ для текущей итерации пропускается и в итоговый список новый элемент не добавится. Если условие вместе с ключевым словом if будет опущено, то это будет эквивалентно условию if True.

В общем случае ПЕРЕМЕННАЯ может быть и не одна: здесь тоже работает распаковка кортежей и списков, в том числе и вложенных.

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

# квадраты чисел
[x*x for x in [1, 2, 3]]
# [1, 4, 9]

# коды прописных букв из заданной строки
[ord(c) for c in "Hello!!" if c.isalpha() and c.islower()]
# [101, 108, 108, 111]

# индексы пар, элементы которых равны друг другу
[i for i, (x, y) in enumerate([(1, 2), (4, 4), (5, 7), (0, 0)]) if x == y]
# [1, 3]

Когда использовать

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

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

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

В чем декларативность генераторов списков

Пример с двумя циклами мог натолкнуть вас на вопрос, а чем же генератор того списка отличается от явно императивного двойного цикла?

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

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

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

Сами циклы for в обоих случаях выглядят одинаково потому, что в Python циклы вообще "более декларативные", чем в языках, имеющих только циклы со счетчиком (который является переменной и явным образом модифицируется). Таким образом, императивным цикл for в Python делает его тело, а не заголовок!


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
с нуля
Разработка веб-приложений на Django
1 декабря 10 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»