Python

В 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?