Как написать генератор-выражения (generator expression) Python

Как написать генератор-выражения (generator expression)

Каждый из нас в процессе обучения или уже при программировании на Python наверняка сталкивался с генераторами списков и, скорее всего что-то слышал о применении в этом языке так называемых функций генераторов или же просто генераторов для оптимизации памяти при работе с очень большими последовательностями данных. Несмотря на схожесть названий, генераторы списков и генераторы вообще в Python существенно отличаются друг от друга использованием оперативной памяти. Так, например, если генераторы списков под каждый элемент своих списков требуют отведения соответствующих объемов оперативной памяти, то Python генераторы вычисляют каждый свой элемент «на лету» при каждой итерации, не требуя при этом абсолютно никакой памяти.

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

Тема применения генераторов весьма и весьма обширна, но в этой статье нами будет рассмотрена лишь одна, наиболее простая разновидность Python генераторовгенераторы-выражений (generator expression), которые при всей своей простоте применения являются, пожалуй, одной из наиболее применяемых вариаций генераторов при программировании на Python. Итак, давайте же сразу приступим к созданию нашего первого генератора-выражения.

Написание генераторов-выражений

Прежде, давайте вспомним, как в коде на Python будет выглядеть определение обычного списка и, создаваемого из него генератора списка:

chisla = [2, 1, 3, 4, 7, 11, 18]    # список
kvadraty = [m**2 for m in chisla]   # генератор списков

print(kvadraty)                     # вывод значения переменной kvadraty в консоль
# [4, 1, 9, 16, 49, 121, 324]

Теперь, давайте проведем в нашем коде небольшое преобразование и заменим квадратные скобки [ и ] в генераторе списков на круглые ( и ):

kvadraty = (m**2 for m in chisla)   # выражение, сформированное генератором

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

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

print(kvadraty)
# <generator object <genexpr> at 0x000001CE85D02030>

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

len(kvadraty)         # вызов функции для определения длины объекта kvadraty
Traceback (most recent call last):           # Отслеживание последнего вызова
  File "<file path>", line 3, in <module>    # Файл «file path» в режиме <module>
# Ошибка: объект типа 'генератор' не имеет длины
TypeError: object of type 'generator' has no len()

Мы также получим ошибку и при попытки обратиться к тому или иному конкретному элементу в объекте нашего генератора-выражения. Для подтверждения этого, давайте попробуем обратиться к первому элементу нашего генератора kvadraty:

print(kvadraty[0])       # обращение к первому элементу объекта генератора-выражения
Traceback (most recent call last):                  # Отслеживание последнего вызова
  File "<file path>", line 3, in <module>           # Файл «file path» в режиме <module>
# Ошибка: объект 'генератор' не может быть проиндексирован
TypeError: 'generator' object is not subscriptable
Из последнего сообщения в консоли Python, наглядно видно, что Индексация объекта генератора-выражения невозможна! Таким образом, единственным доступным нам действием для работы с объектами генераторов-выражений является лишь их итерация внутри стандартных циклов Python или, проще говоря, обработка этих объектов в циклах:
for m in kvadraty:                 # цикл для объекта «kvadraty» с переменной m
    print(m)                       # вывод значения m в консоль
4
1
9
16
49
121
324

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

Зачем использовать генераторы?

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

Давайте убедимся, что элементы последовательностей генераторов-выражений ведут себя именно таким образом, как описано выше. Для этого, в коде ниже нам стоит обратить внимание, что сразу же после инициализации переменной kvadraty выражением, сформированным генератором, в этой переменной создается сугубо лишь только один объект генератора:

kvadraty = (m**2 for m in chisla)   # выражение, сформированное генератором
print(kvadraty)                     # вывод значения переменной kvadraty в консоль
# <generator object <genexpr> at 0x00000107687D1EE0>

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

Теперь, когда мы уже инициализировали переменную kvadraty объектом генератора-выражения, давайте попробуем заменить число 7 в нашем исходном списке (под индексом 4) на число 8:

chisla = [2, 1, 3, 4, 7, 11, 18]    # список
print(chisla)                       # вывод исходного списка в консоль
# [2, 1, 3, 4, 7, 11, 18]

kvadraty = (m**2 for m in chisla)   # выражение, сформированное генератором

chisla[4] = 8                       # меняем значение у 4-го элемента списка
print(chisla)                       # вывод измененного исходного списка в консоль
# [2, 1, 3, 4, 8, 11, 18]

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

# вывод предварительно итерированных элементов объекта генератора на консоль
print(list(kvadraty))
# элементы объекта генератора после изменения исходного списка
# [4, 1, 9, 16, 64, 121, 324]

Благодаря двум последним примерам, мы наглядно убедились в том, что генераторы-выражения не работают до тех пор, пока для них не будет организован цикл. Однако, мы точно с вами несколько разочаруемся при повторном вызове функции конструктора списка list(), так как увидим, что наш список элементов объекта генератора стал пустым:

# вывод предварительно итерированных элементов объекта генератора на консоль
print(list(kvadraty))
# элементы в объекте генератора теперь отсутствуют
[]

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

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

Неполный цикл над генераторами

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

Например, генератор kvadraty, с которым мы работали, был исчерпан:

print(list(kvadraty))
[]                                  # пустой генератор

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

Давайте посмотрим, как это работает на примере нижеследующего кода, где предусмотрим выход из соответствующего цикла в том случае, если при переборе исходного списка chisla обнаружится тот его элемент, значение которого будет превышать 10:

chisla = [2, 1, 3, 4, 7, 11, 18]    # список
kvadraty = (m**2 for m in chisla)   # генератор
for m in kvadraty:                  # определение цикла для генератор генератора
    print(m)                        # вывод переменной m на консоль
    if m > 10:                      # условие для выхода из цикла
        break                       # выход из цикла
4
1
9
16

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

# вывод предварительно итерированных элементов объекта генератора на консоль
print(list(kvadraty))
# [49, 121, 324]

Мы с вами рассмотрели в этой статье уже довольно обширный материал по генераторам-выражений, а по сему, будет не лишнем здесь еще раз повторить наиболее важные моменты, которые нам следует запомнить в отношении взаимодействия с генераторами:

  • Синтаксис определения генераторов-выражений, по сути, очень схож с синтаксисом генераторов списков, но при этом создает сугубо лишь объект, а не последовательность.
  • Итерация является тем единственным действием, которое может быть осуществлено по отношению к объектам генераторов-выражений.
  • Генераторы-выражений воспроизводят значения своих элементов только лишь в ходе каждой итерации, перебирающей эти элементы.
  • После выполнения всех итераций цикла над тем или иным генератором (объектом генератора), считается, что мы полностью его исчерпали, и такой генератор не имеет смысла. Другими словами, после перебора в ходе итераций всех элементов того или иного объекта генератора, этот объект становится пустым навсегда.

Генерация только следующего элемента

Давайте рассмотрим с вами еще один, не совсем привычный способ использования генераторов-выражений с помощью применения к ним встроенной функции Python next(). Предназначение данной функции в основном сводится к получению значений для текущего (еще не итерированного) элемента в объекте генератора-выражений.

Таким образом, функция next() возвращает нам только один, следующий элемент в объекте генератора:

chisla = [2, 1, 3, 4, 7, 11, 18]    # список
kvadraty = (m**2 for m in chisla)   # генератор

print(next(kvadraty))               # вызов следующего элемента4
# 4

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

Таким образом, если для нашего генератора мы будем многократно вызывать функцию next(), то в результате каждого такого ее вызова от данного генератора будет «откусываться» очередной его элемент, значение которого одновременно должно возвращаться все той же функциейnext():

print(next(kvadraty))     # вызов каждого последующего элемента генератора-выражения
# 1
print(next(kvadraty))
# 9
print(next(kvadraty))
# 16
print(next(kvadraty))
# 49
print(next(kvadraty))
# 121
print(next(kvadraty))
# 324

Однако, нам следует иметь в виду, что если мы эту функцию next() вызовем для генератора, вcе элементы которого были уже исчерпаны, то в результате получим исключение StopIteration:

print(next(kvadraty))    # вызов каждого последующего элемента генератора-выражения
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
 StopIteration                       # исключение, останавливающее итерацию

Благодаря вышеприведенному примеру, мы воочию убеждаемся, что вызов функции next() для пустого генератора недопустим. В частности, для нашего случая, исключение StopIteration подтверждает то, что в генераторе kvadraty больше нет элементов. Таким образом, применение данного исключения в коде оказывается весьма полезным инструментарием для того, чтобы определить пусты ли наши генераторы-выражений или же нет:

list(kvadraty)
[]                                  # пустой генератор

Выводы

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

  • Подобно тому, как генераторы списков создают новые списки, генераторы-выражений создают новые объекты генераторов.
  • Генераторы-выражения, представляют собой итерируемые объекты, которые фактически не содержат и не хранят значения. Данные генераторы воспроизводят значения, соответствующие заданным в них выражениям, лишь при переборе элементов этих генераторов в цикле.
  • В сравнении с генераторами списков генераторы-выражений являются более эффективными и гораздо более рациональными с точки зрения использования памяти. Ведь, эти генераторы не используют память для хранения своих значений, которые воспроизводятся буквально на лету**, в процессе их перебора в цикле.
  • Характерной особенностью генераторов-выражений является то, что они воспроизводят одноразовые (способные иссекаться) объекты с ленивой итерацией, позволяющей рассчитывать их значения только при переборе в цикле.
Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09