3.4 Включения и генераторы#
Генераторы#
Общий пункт, который объединял все функции из прошлого модуля и никак ещё не был раскрыт - это использование объекта генератора. Что такое генератор? Мы можем представить для себя генератор как некоторую последовательность, значения которой не хранятся сразу в памяти, а рассчитываются по мере необходимости и сразу же стираются из памяти после использования. Таким образом, генератор можно перебрать в цикле или преобразовать к списку или кортежу. Однако у генератора нельзя обратиться к элементу по индексу и его нельзя повторно перебрать.
Зачем нам нужны генераторы? Во-первых, с помощью генераторов мы можем экономить используемую память и не вычислять сразу промежуточные списки с данными, которые будут передаваться дальше по коду для других вычислений. Рассмотрим такую ситуацию на примере. Допустим, у нас есть функция, которая может посчитать площадь для последовательности из геометрий, причем площадь будет посчитана в единицах измерения, соответствующих системе координат этих объектов. Также у нас есть большой список геометрий в географической системе координат. Если мы передадим список геометрий в функцию в текущей системе координат, то площадь посчитается в градусах, а нам нужны метры. Если мы попытаемся предварительно сделать преобразование геометрий к нужной системе координат, то у нас не хватит памяти, так как у нас очень большой список (условность задачи). Чтобы выйти из этой ситуации, мы можем использовать генераторы. Напишем генератор, который поочередно будет преобразовывать геометрии к нужной системе координат и в каждый момент будет в себе хранить только одно значение. Дальше мы передадим генератор в качестве последовательности в функцию, и функция в свою очередь будет перебирать этот генератор, получать геометрии в нужной для нас системе координат, но при этом не будет значительно влиять на используемую память.
Теперь поговорим, что нужно, чтобы самостоятельно создать такой генератор. Вообще генератор является функцией. Единственное отличие от обычной функции в том, как эта функция возвращает значение. Вместо return используется ключевое слово yield. При использовании yield функция возвращает значение и приостанавливается, а при следующем вызове уже не выполняется с начала, а продолжает свою работу с того места, где остановилась в прошлый раз.
Напишем простой генератор, который будет создавать последовательность квадратов чисел от 0 до n:
def number_generator(n):
for i in range(n):
yield i**2
Теперь, если мы посмотрим на результат вызова этой функции, мы увидим, что получаем не просто конкретное значение, а целый объект - генератор:
number_generator(5)
<generator object number_generator at 0x7fa340a17c60>
Генераторы можно преобразовывать к спискам и кортежам, в таком случае все значения генерируются и сохраняются в соответствующей последовательности:
list(number_generator(5))
[0, 1, 4, 9, 16]
Чаще всего вместо преобразования генератора к какому-либо типу, мы будем просто выполнять по ним циклы, как по любой другой последовательности:
for n in number_generator(5):
print(n)
0
1
4
9
16
Генераторные выражения#
Однако писать функции-генераторы на практике приходится не так часто. В большинстве случаев нам будет достаточно использовать другую конструкцию, которая также создает генераторы - генераторные выражения.
Для создания генераторных выражений используется следующий синтаксис:
(<выражение> for <переменная> in <последовательность>)
По аналогии с синтаксисом счетных циклов это работает так, что мы указываем, какую последовательность мы будем перебирать, придумываем имя переменной, которой на каждой итерации поочередно будут присваиваться значения из последовательности, и указываем выражение, которое должно выполняться на каждой итерации. Сам генератор поочередно возвращает значения указанного выражения.
Посмотрим, как можно переписать пример прошлого генератора:
numbers = range(5)
number_generator = (n**2 for n in numbers)
number_generator
<generator object <genexpr> at 0x7fa340a7cee0>
list(number_generator)
[0, 1, 4, 9, 16]
Также дополнительно в генераторных выражениях может использоваться условие, на основе которого определяется, какое значение будет сгенерировано следующим:
(<выражение> for <переменная> in <последовательность> if <условие>)
Чтобы посмотреть, как это работает на примере, перепишем прошлый генератор, чтобы он возвращал только квадраты четных чисел:
numbers = range(5)
squared_numbers_generator = (n**2 for n in numbers if n % 2 == 0)
list(squared_numbers_generator)
[0, 4, 16]
Последнее, что нужно помнить о генераторах, - это то, что они отрабатывают только единожды. Так, если мы повторно попробуем перебрать генератор, он не вернет ни одного значения:
list(squared_numbers_generator)
[]
Чтобы ещё раз повторно воспользоваться генератором, его нужно будет пересоздать (при использовании функций-генераторов - заново вызвать функцию).
Включения#
Аналогичный синтаксис, как для создания генераторных выражений, может использоваться и для обычного создания списков и словарей (разница только в используемых скобках). Сначала рассмотрим конструкцию, которая называется списковым включением:
[<выражение> for <переменная> in <последовательность> if <условие>]
Списковые включения по сути ничем не отличаются от заполнения списка в цикле и используются для упрощения и сокращения написанного кода.
numbers = range(5)
squared_numbers = [n**2 for n in numbers if n % 2 == 0]
squared_numbers
[0, 4, 16]
Также реализуется включения словарей, позволяющие сгенерировать словарь в цикле в одну строку:
{<ключ>:<значение> for <переменная> in <последовательность> if <условие>}
numbers = range(5)
squared_numbers = {n: n**2 for n in numbers if n % 2 == 0}
squared_numbers
{0: 0, 2: 4, 4: 16}
Практические задания