Когда люди ищут про генераторы в 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 рублей:
Все, словарь готов.
Хотим предупредить вас о том, что со списочными выражениями нужно работать аккуратно, потому что можно быстро свести читаемость кода к нулю.
Например, пример выше можно переписать в 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 есть несколько тонкостей, но мы сначала рассмотрим общий синтаксис, а затем уже про эти тонкости расскажем.
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 цикла, внутреннее состояние генератора сохранилось.
Зачем это нужно? Предположим, вам нужно распарсить (разбить на составляющие) большой сайт, к примеру – Википедию. Скачивать всю Википедию в какую-то структуру данных с дальнейшей обработкой постранично – не получится, у вас не хватит памяти, чтобы держать в ней всю Википедию. Решить задачу можно следующим образом:
Выгода – в том, что вам не нужно держать все ссылки в памяти, генератор выполняется последовательно и хранит в себе только текущую ссылку и указатель на следующую. Кроме того, поскольку 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), происходит следующее, именно в таком порядке:
Эта проблема замечена только в русскоязычных материалах – в англоязычных есть четкое разделение между list comprehensions и iterators. Вероятнее всего, кто-то когда-то подумал, что «ну списочное выражение генерирует список, так что все – генератор», и с этого началась путаница. В любом случае, не называйте генераторами списочные выражения, это – грубая ошибка.
Тезисно: