Знаете ли вы, чем отличается [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”.
Пингбэк: Руководство по использованию *args и **kwargs в Python