Изучаем Python: генераторы, стримы и yield


В Python часто используются generator иyield. Расскажу в этой статье об основных свойствах generator, а также преимуществах работы с ним. Разберёмся в подробностях, как пользоваться yield, чтобы создавать generator

А ещё изучим две другие концепции из информатики: ленивые (отложенные) вычисления и потоки данных (стримы).

Итерируемые объекты

Для начала узнаем, что такое итерируемый объект, а затем разберёмся, как используется generator — в сущности это тоже итератор. 

В Python итерируемый объект — это объект, над которым производятся так называемые проходы (итерации). Например, как в цикле for.

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

lst = [1, 2, 3] for i in lst: print(i) # 1 # 2 # 3 lst = [x+x for x in range(3)] for x in lst: print(x) # 0 # 2 # 4

Так же мы можем проитерировать и символы в строке.

string = "cat" for c in string: print(c) # c # a # t

Ограничение итераций

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

def file_reader(file_path): fp = open(file_path) return fp.read().split("\n")

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

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

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

Генератор

generator — тоже итератор, но его ключевое свойство — ленивые вычисления. Это классическая концепция в информатике, и её переняли многие языки программирования, такие как Haskell. Основная идея этой концепции звучит как вызов-по-необходимости. Отложенные вычисления могут приводить к снижению доступной процессу памяти. 

Генератор — это итератор, который работает в режиме обработки по необходимости. Мы не будем производить вычисления и сохранять значения сразу, а сделаем их “на лету”, когда будут выполняться итерации. 

Доступно два способа создания generator: выражение генератора и функция генератора. 

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

g1 = (x*x for x in range(10)) print(type(g1)) print(next(g1)) print(next(g1))# <type 'generator'> # 0 # 1

Разница тут в том, что мы не вычисляем все значения при создании generator. x*x вычисляется тогда, когда мы итерируем generator.

Чтобы понять разницу, давайте запустим сниппет кода. 

>>> import timeit >>> timeit.timeit('lst = [time.sleep(1) for x in range(5)]', number=2) 10.032547950744629 >>> timeit.timeit('lst = (time.sleep(1) for x in range(5))', number=2) 1.0013580322265625e-05

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

Но в реальности, когда мы создаём generator, time.sleep(1) не выполняется.

Yield

Другой способ создать generator — использовать функцию генератора. Мы берём ключевое слово yield, чтобы вернуть generator в функции.

Давайте посмотрим, как сработает эта функция на fib, где возвращается generator с n числами Фибоначчи. 

def fib(cnt): n, a, b = 0, 0, 1 while n < cnt: yield a a, b = b, a + b n = n + 1 g = fib(10) for i in range(10): print g.next(), # 0 1 1 2 3 5 8 13 21 34

Давайте применим yield , чтобы переписать программу чтения файла, приведённую выше.

def file_reader(file_path): for row in open(file_path, "r"): yield row for row in file_reader('./demo.txt'): print(row),

С таким подходом мы не будем загружать всё содержимое в память. Вместо этого мы загрузим его путём чтения строк.

Поток данных

С генератором мы создадим структуру данных с бесконечным количеством элементов. Этот вид последовательности элементов данных называется в информатике потоком данных (или “стрим”). С его помощью мы можем выражать концепции бесконечных последовательностей математическими методами. 

Например, нам нужна последовательность со всеми числами Фибоначчи. Как мы её получим?

Нам всего-то нужно убрать параметр счётчика из функции выше.

def all_fib(): n, a, b = 0, 0, 1 while True: yield a a, b = b, a + b n = n + 1all_fib_numbers = all_fib()

Вуаля! Мы получаем переменную, которая могла бы отражать все числа Фибоначчи. Давайте напишем общую функцию, чтобы взять n элементов из любого потока. 

def take(n, seq): result = [] try: for i in range(n): result.append(next(seq)) except StopIteration: pass return result

Выражение take(all_fib_numbers, 10) будет в результате возвращать первые 10 чисел Фибоначчи.

Заключение

generator в языке Python — это мощный инструмент для отложенных вычислений, экономии памяти и времени.

Ключевая идея отложенных вычислений — рассчитать значение до того, как оно вам действительно понадобится. Это также помогает нам выражать концепции бесконечных последовательностей.


Перевод статьи Coder’s Cat: What Are Generators, Yields, and Streams in Python?


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.