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



Книга Изучаем 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("
")

Представьте себе, что случится, если мы будем читать большой файл размером в 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 — это мощный инструмент для отложенных вычислений, экономии памяти и времени.


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


472   0  

Comments

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