Представление списков и генераторное выражение в Python

Знаете ли вы, чем отличается [x for x in range(5)] от (x для x в range(5))? Нет? Не страшно! В этой статье мы рассмотрим, что собой представляют представление списков и генераторное выражение в Python и чем они отличаются друг от друга.

Скачивайте книги ТОЛЬКО на русском языке у нас в телеграм канале: PythonBooksRU

Списки в Python

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

Примечание редакции: о массивах в Python читайте в статье “Что такое массивы в Python и как их использовать”.

Список – это тип данных, которыпредставленией может быть представлен в виде коллекции элементов. Простой список выглядит следующим образом – [0, 1, 2, 3, 4, 5]. Элементами списка могут быть любые типы данных и их комбинации:

>>> a = 12
>>> b = "this is text"
>>> my_list = [0, b, ['element', 'another element'], (1, 2, 3), a]
>>> print(my_list)
[0, 'this is text', ['element', 'another element'], (1, 2, 3), 12]

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

>>> a = ['red', 'green', 'blue']
>>> print(a[0])
red

В Python списки являются изменяемыми. Это означает, что вы можете заменять, добавлять или удалять элементы.

Как создавать списки

Существует два распространенных способа создания списков в Python.

Самый ходовой:

>>> my_list = [0, 1, 1, 2, 3]

И менее предпочтительный:

>>> my_list = list()

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

>>> string = "string"
>>> list(string)
['s', 't', 'r', 'i', 'n', 'g']

Что такое представление списков?

Представление списков (англ. list comprehension, этот термин часто не переводится) позволяет создавать списки с меньшим количеством кода. Это истинно питоничный подход: меньше кода – больше эффективности.

Давайте рассмотрим следующий пример. Создадим список с помощью цикла for и функции range().

>>> my_list = []
>>> for x in range(10):
...     my_list.append(x * 2)
...
>>> print(my_list)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

А вот как можно сделать тоже самое с использованием представления списка:

>>> comp_list = [x * 2 for x in range(10)]
>>> print(comp_list)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Конечно, это очень простой пример, приведенный исключительно чтобы дать вам понятие о синтаксисе. Того же результата можно добиться, просто используя функцию list(range(0, 19, 2)). Однако вы можете использовать более сложный модификатор в первой части вычисления или добавить условие, которое будет фильтровать список. Что-то вроде этого:

>>> comp_list = [x ** 2 for x in range(7) if x % 2 == 0]
>>> print(comp_list)
[4, 16, 36]

Другой доступный вариант – использовать list comprehension для объединения нескольких списков и создания списка списков.

Пример:

>>> nums = [1, 2, 3, 4, 5]
>>> letters = ['A', 'B', 'C', 'D', 'E']
>>> nums_letters = [[n, l] for n in nums for l in letters]
# Представление списков комбинирует два простых списка во вложенный список.
>>> print(nums_letters)
>>> print(nums_letters)
[[1, 'A'], [1, 'B'], [1, 'C'], [1, 'D'], [1, 'E'], [2, 'A'], [2, 'B'], [2, 'C'], [2, 'D'], [2, 'E'], [3, 'A'], [3, 'B'], [3, 'C'], [3, 'D'], [3, 'E'], [4, 'A'], [4, 'B'], [4, 'C'], [4, 'D'], [4, 'E'], [5, 'A'], [5, 'B'], [5, 'C'], [5, 'D'], [5, 'E']]
>>>

Попробуем это сделать с текстом или, что более правильно, – со строковым объектом.

>>> iter_string = "some text"
>>> comp_list = [x for x in iter_string if x !=" "]
>>> print(comp_list)
['s', 'o', 'm', 'e', 't', 'e', 'x', 't']

Представления не ограничиваются списками. Можно создавать также представления словарей и множеств.

>>> dict_comp = {x:chr(65+x) for x in range(1, 11)}
>>> type(dict_comp)
<class 'dict'>
>>> print(dict_comp)
{1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'F', 6: 'G', 7: 'H', 8: 'I', 9: 'J', 10: 'K'}
>>> set_comp = {x ** 3 for x in range(10) if x % 2 == 0}
>>> type(set_comp)
<class 'set'>
>>> print(set_comp)
{0, 8, 64, 512, 216}

Когда стоит использовать представление списков

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

Если логика довольно проста, например, ограничена результатами True или False, представление списков может оптимизировать код и позволить сосредоточиться исключительно на логике. Например:

>>> customers = [{"is_kyc_passed": False}, {"is_kyc_passed": True}]
>>> kyc_results = []
>>> for customer in customers:
...     kyc_results.append(customer["is_kyc_passed"])
...
>>> all(kyc_results)
False

Есть много других способов, как это можно реализовать, но давайте рассмотрим пример с представлением списка:

>>> customers = [{"is_kyc_passed": False}, {"is_kyc_passed": True}]
>>> all(customer["is_kyc_passed"] for customer in customers)
False

Преимущества использования list comprehension

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

Для лучшего понимания того, какие преимущества дает представление списков разработчикам Python, можно обратить внимание на следующее:

  • Простота написания и чтения кода. Используя list comprehension для создания списков в Python, разработчики могут сделать свой код более понятным и сократить количество строк, в первую очередь за счет замены циклов for.
  • Ускорение выполнения. Представления списков не только обеспечивают удобный способ написания кода, но и ускоряют его выполнение. Поскольку производительность обычно не считается одним из плюсов использования Python для веб-разработки, этим аспектом не стоит пренебрегать при программировании и рефакторинге.
  • Отсутствие модификации существующих списков. Вызов list comprehension – это создание нового списка, которое Python выполняет без изменения существующего. И этот факт позволяет использовать представление списков в парадигме функционального программирования.

Итерируемые объекты и итераторы

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

Итерируемый объект (англ. iterable) – это “последовательность” данных, которую можно перебирать с помощью цикла. Самым простым наглядным примером итерируемой последовательности может быть список целых чисел – [1, 2, 3, 4, 5, 6, 7]. Однако перебирать в цикле можно и другие типы данных, такие как строки, словари, кортежи, множества и т.д.

В принципе, любой объект, имеющий метод iter(), может быть использован в качестве итерируемого объекта. Проверить наличие метода можно с помощью функции hasattr() в интерпретаторе.

>>> hasattr(str, '__iter__')
True
>>> hasattr(bool, '__iter__')
False

Протокол итератора применяется всякий раз, когда вы выполняете итерацию над последовательностью данных. Например, при использовании цикла for происходит следующее:

  • Сначала на объекте вызывается метод iter(), чтобы преобразовать его в объект-итератор.
  • Метод next() вызывается на объекте-итераторе для получения следующего элемента последовательности.
  • Когда не остается элементов для вызова, возникает исключение StopIteration.
>>> simple_list = [1, 2, 3]
>>> my_iterator = iter(simple_list)
>>> print(my_iterator)
<list_iterator object at 0x7f66b6288630>
>>> next(my_iterator)
1
>>> next(my_iterator)
2
>>> next(my_iterator)
3
>>> next(my_iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

Генераторные выражения

В Python генераторы обеспечивают удобный способ реализации протокола итераторов. Генератор – это итератор, созданный с помощью функции с ключевым словом yield.

Примечание редакции: о yield читайте в статье “Ключевое слово yield в Python”.

Основной особенностью генераторного выражения является оценка элементов по требованию.

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

>>> def my_gen():
...     for x in range(5):
...         yield x

Генераторное выражение в Python – это выражение, которое возвращает генератор (объект генератора).

Генераторное выражение позволяет создавать генератор “на лету” без ключевого слова yield. Однако оно не обладает всей мощью генератора, созданного с помощью функции с yield. Синтаксис и концепция схожи с представлением списка:

>>> gen_exp = (x ** 2 for x in range(10) if x % 2 == 0)
>>> for x in gen_exp:
...     print(x)
4
16
36
64

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

>>> list_comp = [x ** 2 for x in range(10) if x % 2 == 0]
>>> gen_exp = (x ** 2 for x in range(10) if x % 2 == 0)
>>> print(list_comp)
[0, 4, 16, 36, 64]
>>> print(gen_exp)
<generator object <genexpr> at 0x7f600131c410>

Основное преимущество генератора перед списком заключается в том, что он занимает гораздо меньше памяти. Мы можем проверить, сколько памяти занимают оба типа, используя метод sys.getsizeof().

Примечание: в Python 2 использование функции range() не может реально отразить преимущество в размере, так как она хранит в памяти весь список элементов. В Python 3, однако, этот пример жизнеспособен, поскольку функция range() возвращает объект range.

>>> from sys import getsizeof
>>> my_comp = [x * 5 for x in range(1000)]
>>> my_gen = (x * 5 for x in range(1000))
>>> getsizeof(my_comp)
9024
>>> getsizeof(my_gen)
88

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

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

Примечание: Конечно, помимо первоначального использования квадратных скобок, когда Python генерирует списки при помощи list comprehension, есть и другие способы преобразовать генератор в список. Можно использовать, например, функцию list() или оператор распаковки *.

Заключительные мысли

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

Перевод статьи Soner Ayberk “List Comprehensions in Python and Generator Expressions”.

1 комментарий к “Представление списков и генераторное выражение в Python”

  1. Пингбэк: Руководство по использованию *args и **kwargs в Python

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *