- Генераторы списков
- Когда использовать списочные выражения
- Словарные выражения
- Генераторные выражения
Обработка коллекций в основном состоит из сочетаний операций map
и filter
. Отсеять данные по условию, а затем преобразовать и собрать все в конечный список настолько частая задача, что в Python есть особый инструмент сочетающий в себе map
и filter
.
Генераторы списков
Вспомним задачу из предыдущих уроков. Возьмем словарь пользователей, отфильтруем тех, кто старше 10 лет, и получим список их имен.
users = [
{ 'name': 'Igor', 'age': 19 },
{ 'name': 'Danil', 'age': 1 },
{ 'name': 'Vovan', 'age': 4 },
{ 'name': 'Matvey', 'age': 16 },
]
filtered_users = filter(lambda user: user['age'] > 10, users)
names = map(lambda user: user['name'], filtered_users)
list(names) # ['Igor', 'Matvey']
Попробуем решить ту же задачу другим способом:
names = [user['name'] for user in users if user['age'] > 10]
names # ['Igor', 'Matvey']
Вся обработка коллекции умещается в одну короткую строку. Распишем ее подробнее:
[user['name']
for user in users
if user['age'] > 10
]
# ['Igor', 'Matvey']
Теперь код стал похож на запись цикла. Сравните:
names = []
for user in users:
if user['age'] > 10:
names.append(user['name'])
names # ['Igor', 'Matvey']
Код выглядит очень похоже, но есть два различия:
- В первом варианте мы создаем новый список, а во втором — изменяем заранее созданный
- Первый вариант — это выражение, а второй — набор инструкций. Следовательно, первый вариант можно использовать как часть любых других выражений. При этом нам не пришлось объявлять вспомогательные функции, лямбды тоже не понадобились
Выражения вида [… for … in …]
называются списочными выражениями, list comprehensions.
В общем виде списочное выражение описывается так:
[ВЫРАЖЕНИЕ for ПЕРЕМЕННАЯ in ИСТОЧНИК if УСЛОВИЕ]
Рассмотрим этот шаблон подробнее:
ВЫРАЖЕНИЕ
может использоватьПЕРЕМЕННУЮ
и вычисляется в элемент будущего спискаПЕРЕМЕННАЯ
— имя, с которым поочередно связываются элементыИСТОЧНИКА
ИСТОЧНИК
— любой итератор или итерируемый объектУСЛОВИЕ
— выражение, которое используетПЕРЕМЕННУЮ
, вычисляемую на каждой итерации
Если условие оказывается ложным, то вычисление выражения для текущей итерации пропускается — в итоговый список новый элемент не добавится. Если условие вместе с ключевым словом 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]
# Пример посложнее: отфильтруем во вложенных списках четные элементы, затем оставим списки длиннее трех элементов
list_of_lists = [[1, 2, 3, 5], [7, 11, 8, 0], [21, 12, 2, 7, 1], [1, 3]]
# Генерируем внутренний список списков и оставляем только нечетные элементы
# Отфильтруем список списков и оставим только списки длиннее 3
[ x for x in [[elem for elem in l if elem % 2 == 1] for l in list_of_lists] if len(x) >= 3]
# [[1, 3, 5], [21, 7, 1]]
Когда использовать списочные выражения
Выше мы увидели, что списочные выражения не отменяют все встроенные функции для работы с итераторами. Одно с другим отлично сочетается.
С другой стороны, лучше не смешивать их с функциями map()
и filter()
— это как раз взаимозаменяемые сущности. Еще не стоит их смешивать с какими-либо побочными эффектами.
Это касается не только кода с функциями map()
и filter()
, но и вообще любых конвейеров обработки. Стоит разделять код, ответственный за работу с побочными эффектами и чистую обработку. Например, ввод-вывод — это один из основных видов побочных эффектов. Он может находиться в начале конвейера или в его конце, но не в середине.
Словарные выражения
Наряду с созданием списков через выражения, в Python существует подобный способ создавать множества и словари. Главное отличие, что теперь выражение заключено в фигурные {}
скобки.
squares = {x * x for x in range(10)}
squares # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
Создание словарей выглядят очень похоже на создание множеств. Разница заключается в том, как описывается элемент словаря.
Нужно сгенерировать не только значение, но и ключ. При этом ключ надо указать через двоеточие:
char_positions = {char: pos for pos, char in enumerate("Hello, World!")}
char_positions
# {'H': 0, 'e': 1, 'l': 10, 'o': 8, ',': 5, ' ': 6, 'W': 7, 'r': 9, 'd': 11, '!': 12}
char_positions['o']
# 8
Обратите внимание, что в этом примере ключ 'l'
имеет значение 10
. Посмотрим, какие значения имели char
и pos
во время генерации. Для простоты будем смотреть только на позиции символа 'l'
:
[(char, pos) for pos, char in enumerate("Hello, World!") if char == 'l']
# [('l', 2), ('l', 3), ('l', 10)]
Как можно заметить, 'l'
встречается в исходной строке три раза — в последнем случае как раз в позиции 10
. При генерации словаря используется последнее значение для каждого из ключей, будто словарь был заполнен в подобном цикле:
char_positions = {}
for pos, char in enumerate("Hello, World!"):
char_positions[char] = pos
char_positions
# {'H': 0, 'e': 1, 'l': 10, 'o': 8, ',': 5, ' ': 6, 'W': 7, 'r': 9, 'd': 11, '!': 12}
В примере выше порядок ключей получается тот же самый — это порядок первого появления соответствующего символа в строке. Последующие перезаписи значений этот порядок не изменят. Словари в Python запоминают порядок добавления ключей, но не порядок последующих изменений значений.
Генераторные выражения
Хоть списочные и словарные выражения почти всегда заменяют использование map()
и filter()
, у них есть один главный недостаток - они вычисляются сразу. Ранее мы говорили, что многие функции для работы с коллекциями в питоне ленивые. Так мы можем собирать конвейеры обработки и "протаскивать" данные через них по одному, без создания промежуточных коллекций.
Но list и dict comprehensions всегда сразу создают коллекцию, что может быть непрактично при работе с большими данными. Более того, зачастую последовательности не нужно вычислять целиком, в конце обработки данные соберутся в какой-то вывод.
Для решения задач выше, но в ленивом подходе, существуют генераторные выражения. Выглядят они как списочные выражения, разница только в круглых скобках вместо квадратных:
[x * x for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
(x * x for x in range(10))
# <generator object <genexpr> at 0x7fe76f7e5db0>
Как видите, результатом вычисления второго выражения является не список, а generator object
— это объект-генератор, уже знакомый нам ленивый итератор.
Часто можно встретить генераторное выражение в таком месте кода, где интерпретатор может однозначно понять, где границы этого выражения. Самый частый пример — генераторное выражение в роли единственного аргумента функции:
f((… for … in …))
В подобных случаях скобки вокруг самого выражения можно опустить. Такое избавление от лишних скобок часто делает код еще более лаконичным:
any(x > 100 for x in range(1000000))
# True
Код выше можно перевести так:
Есть ли любой икс больше ста среди иксов в диапазоне от нуля до миллиона?
Это выражение вычислится мгновенно, а числа будут проверяться по одному за раз.
А теперь представим, что мы использовали any([… for …])
. В таком случае Python тоже искал бы первое значение True
в списке, но предварительно построил бы в памяти список в миллион элементов.
Старайтесь применять генераторные выражения везде, где это возможно. Использовать объекты-генераторы могут практически любые функции, которые работают с последовательностями в том или ином виде. Даже при вызове функции для пачки аргументов лучше использовать генераторное выражение:
print(*(x for x in "Hello World!" if x.isupper()))
# => H W
И уж тем более стоит использовать генераторные выражения посреди выражений с list
, set
и dict
. Генераторные выражения регулярно используются вместе с sum
, any
, all
, а также среди конвейеров на основе map()
или filter()
.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.