Представление списков и генераторное выражение в 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”.

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

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