logo
Ещё

Генераторы Python

Когда люди ищут про генераторы в Python 3 или Python 2, они имеют в виду либо генераторы словарей, либо просто генераторы. Разница между этими генераторами – существенная: генератор словарей/списков возвращает последовательность объектов, объединенную в список или словарь; обычные генераторы возвращают итераторы, с помощью которых можно хранить огромные последовательности объектов, не забивая при этом память. Ниже мы расскажем про оба вида и объясним, откуда возникла путаница.

Списочные выражения: [], {}

Генераторы списков на самом деле называются списочными выражениями (list comprehension). Именно списочные выражения легли в основу однострочников (one-liners) – сложных однострочных команд, за которые Питон и хвалят, и ругают. Суть: списочные выражения в Python предоставляют возможность запихнуть циклы for с созданием элементов списка или словаря в специальную упрощенную конструкцию. В for цикле можно указать дополнительные условия или еще один цикл.

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

Синтаксис:

variable = [i*i for i in range(10) if i > 5]

Итак, что здесь происходит? Скобки [] создают список. Внутри скобок с использованием циклов перебираются некоторые элементы, i in range(10), вместо range(10) можно использовать любой итерируемый объект (в том числе и обычные генераторы, о которых мы будем говорить ниже). Для каждого элемента выполняется некоторое действие, после которого элемент записывается в список, в нашем случае это возведение в квадрат путем умножения i на само себя (никто не мешает написать просто i – в этом случае i будет просто записываться в список). Дополнительно генераторы предоставляют возможность добавить условие, при котором элемент добавляется в список. Это условие проверяется на стадии *i in range 10*, то есть в сгенерированном выше списке будут числа 36, 49, 64, 81 – исходные i от 0 до 5 включительно не пройдут через условие i < 5. Часть … if i > 5 – не обязательная.

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

variable = {key: value for key, value in dict.items() if value > 5}

Усложнения связаны непосредственно с архитектурой словарей. В выражении, описанном выше, мы создаем новый словарь из исходного (dict). Для этого мы сначала разбиваем его на кортежи (ключ, значение), для чего можно использовать стандартную библиотеку Python, если точнее – встроенный метод словарей .items(). После этого мы распаковываем кортеж по схеме a, b = (c, d), в результате чего получаем 2 переменные: key и value. Теперь мы в новый словарь помещаем пару «ключ: значение» при условии, что значение – больше 5. Как и в случае со списками, if value > 5 можно пропустить.

Когда вы создаете словарь списочным выражением, очень удобно использовать встроенный метод zip(), который берет произвольное количество итерируемых объектов и разбивает их на кортежи.

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

customers_id = ["id" + str(random.randint(1000, 9999)) for _ in range(10)]

customers_cart = [random.randint(100, 15000) for _ in range(10)]

customers_id_with_cart = {key: value for key, value in zip(customers_id, customers_cart) if value > 1000}

print(customers_id_with_cart)

Сначала импортируем random, чтобы сгенерировать 2 списка. Генерируем эти самые списки – в customers_id лежат айдишники клиентов в виде строк (результат работы random() нужно явно преобразовывать в str), в customers_cart лежат суммы, на которые эти клиенты закупились на нашем сайте. В реальной жизни эти списки придут вам из какого-нибудь API, но у нас под рукой таких списков нет, так что генерируем их случайно. Теперь создаем словарь, в котором ключами будут id клиентов, а значениями – суммы покупок, при этом учитываются только клиенты, которые закупились более чем на 1000 рублей:

  1. Поскольку у нас есть 2 списка, а генератору нужно скормить 2 значения, логично будет эти списки объединить в кортеж: zip(customers_id, customers_cart).
  2. На выходе получаем итерируемый объект из кортежей – отлично, пишем основную конструкцию: key: value for key, value in zip(customers_id, customers_cart).
  3. Нужно отсечь тех, кто не прошел по сумме – добавляем условие: if value > 1000.

Все, словарь готов.

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

Например, пример выше можно переписать в 3 строки:

import random

customers_id_with_cart = {key: value for key, value in zip(["id" + str(random.randint(1000, 9999)) for _ in range(10)], [random.randint(100, 15000) for _ in range(10)]) if value > 1000}

print(customers_id_with_cart)

Мы просто поместили внутрь списочного выражения, создающего словарь, еще 2 списочных выражения, создающих списки (в функцию zip()). Работает ли это? Да. Понятно ли, что здесь написано? Не особо.

Поэтому соблюдайте правило «Не более 80 символов на строку» и не городите списочные выражения одно внутри другого.

Настоящие генераторы: yield

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

def brand_new_iterator():

for i in range (100000000000000):

yield i

list1, list2 = [], []

itrtr = brand_new_iterator()

for i in range(10):

list1.append(next(itrtr))

for i in range(10):

list2.append(next(itrtr))

print(list1)

print(list2)

Вывод:


Итак, что здесь происходит? Чтобы создать итератор, нам нужно создать обычную функцию, только вместо обычного return нам нужно указать yield.

В процессе создания итераторов генерируется объект типа Generator, который спокойно лежит себе в переменной (itrtr = brand_new_iterator()). Вызвать итератор можно методом next, вызовов next может быть несколько (это видно из того, что next расположен в цикле). Что происходит, когда мы исполняем next(itrtr)? В первый раз запускается функция, которая входит в цикл. Когда она в цикле встречает yield, то переменная i возвращается наружу (в вызвавшую строку), и исполнение функции приостанавливается. Последующие вызовы функций через next снова запускают код с того места, где функция в прошлый раз остановилась, и снова она остановится согласно следующему вызову yield. Отсюда и результат: сначала генератор в коде выше сгенерировал последовательность от 0 до 9 для первого списка, затем он сгенерировал последовательность от 10 до 19 для второго списка – несмотря на то, что мы использовали 2 цикла, внутреннее состояние генератора сохранилось.

Зачем это нужно? Предположим, вам нужно распарсить (разбить на составляющие) большой сайт, к примеру – Википедию. Скачивать всю Википедию в какую-то структуру данных с дальнейшей обработкой постранично – не получится, у вас не хватит памяти, чтобы держать в ней всю Википедию. Решить задачу можно следующим образом:

  1. Заводим файлик, в котором будем хранить все ссылки на все статьи Википедии (этот файлик уже будет огромным).
  2. Создаем итератор, который будет проходиться по файлу, за каждую итерацию он будет возвращать новую ссылку.
  3. Пишем основной скрипт, который будет брать ссылку от итератора и парсить страницу.

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

Напоследок – еще два вопроса, которые вызывают трудности.

Первый связан с несколькими yield в одном итераторе. Например:

def brand_new_iterator():

i = 0

while True:

yield i

i += 2

yield i

i += 1

list1 = []

itrtr = brand_new_iterator()

for i in range(10):

list1.append(next(itrtr))

print(list1)

Вывод:


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

Когда мы снова вызываем его, он доходит до следующего yield, снова возвращает значение и приостанавливается. Поэтому числа в выводе имеют разный инкремент: +2, +1, +2, +1…

Второй вопрос – это передача значения в генератор. Например:

def brand_new_iterator():

i = 10

while True:

x = yield i

i += x

list1 = []

itrtr = brand_new_iterator()

itrtr.send(None)

for i in range(10):

list1.append(itrtr.send(i))

print(list1)

Вывод:

Здесь мы пользуемся специальным методом итератора – .send() – чтобы послать в итератор значение. Поскольку значение нужно как-то получить, в самом итераторе мы пишем x = yield i. Строка itrtr.send(None) нужна для того, чтобы «завести» итератор – без нее компилятор будет ругаться на то, что нельзя передавать значения в только что созданный итератор. Каждый раз, когда в цикле .send(i), происходит следующее, именно в таком порядке:

  1. Итератор возвращает значение своего i.
  2. В итератор подается значение i из основного цикла.
  3. К i из итератора прибавляется пришедшее значение.
  4. Итератор ждет следующего вызова.

Откуда пошла привычка называть списочные выражения генераторами?

Эта проблема замечена только в русскоязычных материалах – в англоязычных есть четкое разделение между list comprehensions и iterators. Вероятнее всего, кто-то когда-то подумал, что «ну списочное выражение генерирует список, так что все – генератор», и с этого началась путаница. В любом случае, не называйте генераторами списочные выражения, это – грубая ошибка. 

Что почитать?

Вывод

Тезисно:

  • Иногда генераторами называют списочные выражения – специальные конструкции языки Python, позволяющие быстро создать список или словарь.
  • Списочные выражения – очень удобная вещь, но старайтесь делать их максимально простыми, иначе читабельность кода резко упадет.
  • Настоящие генераторы – это объекты, которые итеративно возвращают значение и сохраняют внутри себя состояние.
  • Генератор имеет такой же синтаксис, как функция, но вместо return используется yield.
  • В генераторе может быть несколько yield, генератор может получать значения через .send(). Но это – advanced-техники, если вы только начали изучать Python, можете отложить эти детали на потом.
Часто ищут