В английском языке конструкция, о которой мы поговорим, называется list comprehension. На русском языке — в обучающих статьях, в учебниках, на форумах — все переводят это словосочетание по-разному. Устоявшегося перевода нет. Чаще всего list comprehension называют генератором списков, но это не совсем корректно. Дело в том, что в Python есть понятие генератора, которое никак не связано с list comprehension. Поэтому на более серьезном уровне это понятия переводят как представление списков. Поскольку в этой статье речь пойдет в том числе о генераторах, мы будем называть list comprehension представлением списков.
Примечание AskMentor.io.

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

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

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

Советы

1. Вы можете использовать любой итерируемый объект в представлении списков

Базовая форма представления списков — создание списка из итерируемого объекта. Итерируемыми объектами называются любые объекты Python, по элементам которых можно проитерироваться.

Синтаксис указан ниже.

[выражение for элемент in итерируемый объект]

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

>>> # Список целых чисел
>>> integers = [1, 2, 3, 4, 5, 6]
>>> # Создание списка квадратов и третьих степеней
>>> powers = [(x*x, pow(x, 3)) for x in integers]
>>> print(powers)

[(1, 1), (4, 8), (9, 27), (16, 64), (25, 125), (36, 216)]

В предыдущем примере в качестве итерируемого объекта использовался список. Но мы не должны забывать, что итерируемым является также множество других объектов. Из стандартных типов данных итерируемыми являются списки, множества, словари и строки. Из других типов данных итерируемыми являются объекты range, map, filter, а также Series и DataFrame из Pandas. В следующем куске кода отображены примеры использования некоторых из этих объектов.

>>> # Использование объекта range
>>> integer_range = range(5)
>>> [x*x for x in integer_range]

[0, 1, 4, 9, 16]


>>> # Использование объекта Series
>>> import pandas as pd
>>> pd_series = pd.Series(range(5))
>>> print(pd_series)

0    0
1    1
2    2
3    3
4    4
dtype: int64


>>> [x*x for x in pd_series]

[0, 1, 4, 9, 16]

2. Пишите условие, по которому хотите выбрать элементы, если это необходимо

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

[выражение for элемент in итерируемый объект if условие]

Проверка условия происходит в операторе if, который следует за итерируемым объектом. В коде ниже отображен элементарный пример использования подобной конструкции.

>>> # Тот же список целых чисел
>>> integers = [1, 2, 3, 4, 5, 6]
>>> # Создание списка квадратов только для чётных чисел
>>> squares_of_evens = [x*x for x in integers if x % 2 == 0]
>>> print((squares_of_evens))

[4, 16, 36]

3. Используйте условные выражения

Представления списков работают и с тернарными операторами. Они имеют следующий синтаксис:

[выражение if  условие else выражение for элемент in итерируемый объект]

Такой вариант использования напоминает предыдущий, но не стоит их путать. В прошлом варианте применения мы использовали лишь часть условного выражения — оператор if. Пример в коде ниже:

>>> # Список целых чисел
>>> integers = [1, 2, 3, 4, 5, 6]
>>> # Создание списка чисел, куда записывается квадрат, если число
>>> # четное, и куба, если нечетное
>>> custom_powers = [x*x if x % 2 == 0 else pow(x, 3) for x in integers]
>>> print(custom_powers)

[1, 4, 27, 16, 125, 36]

4. Используйте вложенные циклы for, если итерируемый объект является вложенной структурой данных

Хоть подобное и не так часто встречается, элементы итерируемого объекта сами могут быть итерируемыми объектами. Если вам нужно работать с элементами вложенных итерируемых, воспользуйтесь вложенным циклом for. Синтаксис следующий:

[выражение for наружный элемент in итериуремый объект for внутренний элемент in наружный элемент]

#Это эквивалентно:
for наружный элемент in итерируемый объект:
	for наружный элемент in внутренний элемент:
		выражение

В коде ниже приведен пример использования вложенного цикла for в представлении списка:

>>> # Список кортежей
>>> prices = [('$5.99', '$4.99'), ('$3.5', '$4.5')]
>>> # Развернутый список цен
>>> prices_formatted = [float(x[1:]) for price_group in prices for x in price_group]
>>> print(prices_formatted)

[5.99, 4.99, 3.5, 4.5]

5. Заменяйте функции высшего порядка

Некоторые больше привыкли к функциональному программированию. Одна из его фишек — функции высшего порядка, то есть функции, которые принимают в качестве входных и выходных аргументов другие функции. Распространенные функции высшего порядка — map() и filter().

>>> # Список целых чисел
>>> integers = [1, 2, 3, 4, 5]
>>> # Использование map
>>> squares_mapped = list(map(lambda x: x*x, integers))
>>> squares_mapped

[1, 4, 9, 16, 25]


>>> # Использование спискового включения
>>> squares_listcomp = [x*x for x in integers]
>>> squares_listcomp

[1, 4, 9, 16, 25]


>>> # Использование filter
>>> filtered_filter = list(filter(lambda x: x % 2 == 0, integers))
>>> filtered_filter

[2, 4]


>>> # Использование спискового включения
>>> filterd_listcomp = [x for x in integers if x % 2 == 0]
>>> filterd_listcomp

[2, 4]

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

Предостережения

1. Не забывайте о функции list()

Некоторые люди считают, что списковые включения — крутая и очень «пайтоновская» особенность, которую можно использовать, чтобы показать свой скилл в Python. Они используют ее даже там, где лучше использовать другие варианты. Один из них — функция list(). Рассмотрите примеры ниже.

>>> # Создание списка из объекта range
>>> numbers = [x for x in range(5)]
>>> print(numbers)

[0, 1, 2, 3, 4]


>>> # Создание списка символов в нижнем регистре из строки
>>> letters = [x.lower() for x in 'Smith']
>>> print(letters)

['s', 'm', 'i', 't', 'h']

В примере выше в качестве итерируемых объектов мы использовали range и строку соответственно. Но оба типа объектов являются итерируемыми, а функция list() принимает итерируемые объекты для создания нового списка напрямую. Лучшие и более «чистые» решения — ниже.

>>> # Создание списка из объекта range
>>> numbers = list(range(5))
>>> print(numbers)

[0, 1, 2, 3, 4]


>>> # Создание списка символов в нижнем регистре из строки
>>> letters = list('Smith'.lower())
>>> print(letters)

['s', 'm', 'i', 't', 'h']

2. Не забывайте о генераторных выражениях

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

Чтобы создать генератор, можно определить генераторную функцию. Однако можно использовать и следующую конструкцию под названием генераторное выражение.

(выражение for элемент in итерируемый объект)

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

Рассмотрим следующий простой пример. Нам нужно посчитать сумму квадратов первых десяти миллионов целых чисел. Если мы используем представление списков, код будет примерно следующий.

>>> # Создание списка квадратов
>>> squares = [x*x for x in range(10_000_000)]
>>> # Вычисление суммы
>>> sum(squares)

333333283333335000000


>>> squares.__sizeof__()

81528032

Как показано выше, объект списка занимает 81528032 байт. Выполним те же самые операции через генератор.

>>> # Создание генератора квадратов
>>> squares_gen = (x*x for x in range(10_000_000))
>>> # Вычисление суммы
>>> sum(squares_gen)

333333283333335000000


>>> squares_gen.__sizeof__()

96

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

Заключение

В этой статье мы рассмотрели несколько ключевых принципов использования списковых включений. Как вы увидели, эти советы и предостережения достаточно просты. Думаю, теперь вы сможете использовать представления списков в желаемых сценариях. Кратко повторим основные идеи.

  • Используйте любые итерируемые объекты в представлении списков. В Python существует множество итерируемых объектов и вам стоит выходить за рамки базовых, таких как списки и кортежи.
  • Применяйте фильтрующие условия, если хотите сохранять определенные элементы в списке.
  • Используйте условные выражения, если вам необходим не один способ добавления значения в формирующийся объект.
  • Используйте вложенный цикл for, если вы имеете дело с вложенной структурой данных.
  • Используйте представление списков для замены функций высшего порядка в разных случаях.
  • Не забывайте о функции list(), которая принимает итерируемый объект для создания нового объекта списка. Если вы работаете с итерируемым объектом напрямую, этот способ лучше, чем использовать представление списков.
  • Не забывайте о генераторных выражениях, которые по синтаксису напоминают представления списков. Это эффективный по памяти способ работы с большой последовательностью объектов. В отличие от генераторов, списки создаются заранее для дальнейшей индексации и осуществления доступа, что требует немалого объема памяти, если в списке слишком много элементов.  

Перевод статьи: The Do’s and Don’ts of Python List Comprehension