Python: коллекции, часть 4/4: Выражения-генераторы: определение и синтаксис
December 21st 2021
1. Определения и классификация
1.1 Что и зачем
- Генераторы выражений предназначены для компактного и удобного способа генерации коллекций элементов, а также преобразования одного типа коллекций в другой.
- В процессе генерации или преобразования возможно применение условий и модификация элементов.
- Генераторы выражений являются синтаксическим сахаром и не решают задач, которые нельзя было бы решить без их использования.
1.2 Преимущества использования генераторов выражений
- Более короткий и удобный синтаксис, чем генерация в обычном цикле.
- Более понятный и читаемый синтаксис чем функциональный аналог сочетающий одновременное применение функций map(), filter() и lambda.
- В целом: быстрее набирать, легче читать, особенно когда подобных операций много в коде.
1.3 Классификация и особенности
Сразу скажу, что существует некоторая терминологическая путаница в русских названиях того, о чем мы будем говорить.
В данной статье используются следующие обозначения:
- выражение-генератор (generator expression) — выражение в круглых скобках которое выдает создает на каждой итерации новый элемент по правилам.
- генератор коллекции — обобщенное название для генератора списка (list comprehension), генератора словаря (dictionary comprehension) и генератора множества (set comprehension).
В отдельных местах, чтобы избежать нагромождения терминов, будет использоваться термин «генератор» без дополнительных уточнений.
2. Синтаксис
Для начала приведем иллюстрацию общего синтаксиса выражения-генератора.
Важно: этот синтаксис одинаков и для выражения-генератора и для всех трех типов генераторов коллекций, разница заключается, в каких скобках он будет заключен (смотрите предыдущую иллюстрацию).
Общие принципы важные для понимания:
- Ввод — это итератор — это может быть функция-генератор, выражение-генератор, коллекция — любой объект поддерживающий итерацию по нему.
- Условие — это фильтр при выполнении которого элемент пойдет в финальное выражение, если элемент ему не удовлетворяет, он будет пропущен.
- Финальное выражение — преобразование каждого выбранного элемента перед его выводом или просто вывод без изменений.
2.1 Базовый синтаксис
list_a = [-2, -1, 0, 1, 2, 3, 4, 5] # Пусть у нас есть исходный список
list_b = [x for x in list_a] # Создадим новый список используя генератор списка
print(list_b) # [-2, -1, 0, 1, 2, 3, 4, 5]
print(list_a is list_b) # False - это разные объекты!
По сути, ничего интересного тут не произошло, мы просто получили копию списка. Делать такие копии или просто перегонять коллекции из типа в тип с помощью генераторов особого смысла нет — это можно сделать значительно проще применив соответствующие методы или функции создания коллекций (рассматривались в первой статье цикла).
Мощь генераторов выражений заключается в том, что мы можем задавать условия для включения элемента в новую коллекцию и можем делать преобразование текущего элемента с помощью выражения или функции перед его выводом (включением в новую коллекцию).
2.2 Добавляем условие для фильтрации
Важно: Условие проверяется на каждой итерации, и только элементы ему удовлетворяющие идут в обработку в выражении.
Добавим в предыдущий пример условие — брать только четные элементы.
# if x % 2 == 0 - остаток от деления на 2 равен нулю - число четное
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x for x in list_a if x % 2 == 0]
print(list_b) # [-2, 0, 2, 4]
Мы можем использовать
несколько условий, комбинируя их логическими операторами:
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x for x in list_a if x % 2 == 0 and x > 0]
# берем те x, которые одновременно четные и больше нуля
print(list_b) # [2, 4]
2.3 Добавляем обработку элемента в выражении
Мы можем вставлять не сам текущий элемент, прошедший фильтр, а результат вычисления выражения с ним или результат его обработки функцией.
Важно: Выражение выполняется независимо на каждой итерации, обрабатывая каждый элемент индивидуально.
Например, можем посчитать квадраты значений каждого элемента:
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x**2 for x in list_a]
print(list_b) # [4, 1, 0, 1, 4, 9, 16, 25]
Или посчитать длины строк c помощью функции len()
list_a = ['a', 'abc', 'abcde']
list_b = [len(x) for x in list_a]
print(list_b) # [1, 3, 5]
2.4 Ветвление выражения
Обратите внимание: Мы можем использовать (начиная с Python 2.5) в выражении конструкцию
if-else для ветвления финального выражения.
В таком случае:
- Условия ветвления пишутся не после, а перед итератором.
- В данном случае if-else это не фильтр перед выполнением выражения, а ветвление самого выражения, то есть переменная уже прошла фильтр, но в зависимости от условия может быть обработана по-разному!
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x if x < 0 else x**2 for x in list_a]
# Если x-отрицательное - берем x, в остальных случаях - берем квадрат x
print(list_b) # [-2, -1, 0, 1, 4, 9, 16, 25]
Никто не запрещает
комбинировать фильтрацию и ветвление:
list_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = [x**3 if x < 0 else x**2 for x in list_a if x % 2 == 0]
# вначале фильтр пропускает в выражение только четные значения
# после этого ветвление в выражении для отрицательных возводит в куб, а для остальных в квадрат
print(list_b) # [-8, 0, 4, 16]
Этот же пример в виде циклаlist_a = [-2, -1, 0, 1, 2, 3, 4, 5]
list_b = []
for x in list_a:
if x % 2 == 0:
if x < 0:
list_b.append(x ** 3)
else:
list_b.append(x ** 2)
print(list_b) # [-8, 0, 4, 16]
2.5 Улучшаем читаемость
Не забываем, что в Python синтаксис позволяет использовать переносы строк внутри скобок. Используя эту возможность, можно сделать синтаксис генераторов выражений более легким для чтения:
numbers = range(10)
# Before
squared_evens = [n ** 2 for n in numbers if n % 2 == 0]
# After
squared_evens = [
n ** 2
for n in numbers
if n % 2 == 0
]
3. Аналоги в виде цикла for и в виде функций
Как уже говорилось выше, задачи решаемые с помощью генераторов выражений можно решить и без них. Приведем другие подходы, которые могут быть использованы для решения тех же задач.
Для примера возьмем простую задачу — сделаем из списка чисел список квадратов четных чисел и решим ее с помощью трех разных подходов:
3.1 Решение с помощью генератора списка
numbers = range(10)
squared_evens = [n ** 2 for n in numbers if n % 2 == 0]
print(squared_evens) # [0, 4, 16, 36, 64]
3.2. Решение c помощью цикла for
Важно: Каждый генератор выражений можно переписать в виде цикла for, но не каждый цикл for можно представить в виде такого выражения.
numbers = range(10)
squared_evens = []
for n in numbers:
if n % 2 == 0:
squared_evens.append(n ** 2)
print(squared_evens) # [0, 4, 16, 36, 64]
В целом, для очень сложных и комплексных задач, решение в виде цикла может быть понятней и проще в поддержке и доработке. Для более простых задач, синтаксис выражения-генератора будет компактней и легче в чтении.
3.3. Решение с помощью функций.
Для начала, замечу, что выражение генераторы и генераторы коллекций — это тоже функциональный стиль, но более новый и предпочтительный.
Можно применять и более старые функциональные подходы для решения тех же задач, комбинируя map(), lambda и filter().
numbers = range(10)
squared_evens = map(lambda n: n ** 2, filter(lambda n: n % 2 == 0, numbers))
print(squared_evens) # <map object at 0x7f661e5dba20>
print(list(squared_evens)) # [0, 4, 16, 36, 64]
# Примечание: в Python 2 в переменной squared_evens окажется сразу список, а в Python 3 «map object», который мы превращаем в список с помощью list()
Несмотря на то, что подобный пример вполне рабочий, читается он тяжело и использование синтаксиса генераторов выражений будет более предпочительным и понятным.