Яндекс.Метрика

понедельник, 15 июля 2013 г.

Зачем нужны генераторы в Питоне (What for Python generators are invented)

Решила написать техническую статью, а то поговаривают, что блог стал совсем несерьезным. Придадим серьезности моему бложику.

Кстати, меня с недавних пор начал мучить вопрос как правильно «бложик» или «бложек»? Вы как думаете?

А теперь, генераторы.


Шучу.

1. Что такое генератор?

Напомню, что генератором-функцией является та функция, которая содержит в себе инструкцию yield. При вызове метода next объекта этой функции мы получим значение, указанное за yield, и весь контекст, включая значение локальных переменных, будет запомнен в том состоянии, в котором он находятся при выполнении строчки с yield. Например.

def gener(stop):
    i = 0
    while i < stop:
        yield i
        i += 1

Так, создав объект

g = gener(7)

и вызвав метод next

g.next()

получим сначала

0

После чего параметры функции i = 0 и то, что выполнение функции находится внутри тела цикла, будет запомнено до следующего вызова next. Следующий вызов next, вернет 1.

g.next()
1

Так будет происходить, пока условие i < stop выполняется, а именно до тех пор, пока i не достигнет значения 7.

...
g.next()
6

g.next()
StopIteration

Таким образом, при достижении «конца» итераций, возвращается исключение StopIteration. Выход из генератора можно вызвать явно, написав где-либо в функции инструкцию return, при достижении которой опять же вернется исключение StopIteration.

То есть объявление типа

def gener(stop):
    i = 0
    while i < stop:
        yield i
        i += 1
    return

равнозначно предыдущей функции gener.

Часть 2. Зачем нужны такие на первый взгляд бесполезные функции, возвращающие генераторы?

Известно, что объекты типа питоновских списков занимают память пропорционально количеству элементов. Скажем список [0, 1, 2, 3, 4, 5, 6] будет затрачивать память как минимум под шесть целочисленных значений. Если нам нужно будет итерироваться по такому списку, то мы можем явно написать что-то вроде

for i in [0, 1, 2, 3, 4, 5, 6]
    print i

и получить на стандартный вывод последовательность от 0 до 6.

Если же нам понадобится итерироваться по списку с гораздо большим количеством целых чисел, скажем, от нуля до нескольких миллиардов с шагом 1, то неужели придется хранить все эти числа в памяти? Это не нужно. Достаточно знать начало, конец и шаг итераций. Для этого в Питоне 2.x используют функцию xrange, она возвращает генератор, который в каждый момент времени хранит только начальное, конечное значения, шаг, и текущее значение, что намного меньше, чем несколько миллиардов чисел. Такую функцию можем и написать мы сами, используя yield. Возможно, правда, xrange будет работать чуть эффективнее, потому что разработчики постарались как-нибудь ее оптимизировать.

И это еще не всё. Что, если мы захотим хранить объект из бесконечного числа субобъектов? Мы можем оперировать этим понятием в жизни, но не в программировании? Не может быть! Конечно не может. Чтобы убедиться в этом, достаточно допустить ошибку в предыдущей функции.

def gener():
    i = 0
    while True:
        yield i
        i += 1

Вы видите описанный бесконечный цикл. Итерироваться по объекту, возвращенному такой функцией, можно бесконечно. Сам объект будет символом бесконечного множества.

g = gener()

И все дела.

Часть 3. Загадочный метод send.

У объектов-генераторов еще есть метод send, который на первый взгляд тоже кажется довольно бесполезным ;)

Если вызвать метод send у генератора, тогда yield внутри функции-генератора вернет значение, отправленное методом send. По умолчанию yield возвращает значение None.

Напишем такую функцию-генератор. Если yield возвращает None, то к i прибавляется единица, если же нет, то i становится равным x. Так можно в генератор посылать новое значение, с которого начнет итерироваться i снова.

def gener():
    i = yield
    while True:
        x = yield i
        if not x:
            i += 1
        else:
            i = x


g = gener()

g.next()
0

g.send(2)
2

g.next()
3

И т. д. С помощью send в данном случае можно определять значение, с которого нужно итерироваться, создав только один объект-генератор.

Вообще мне тут сказали, что генератор, в который можно посылать начальное значение не очень имеет смысл, так как вместо этого можно создать просто еще один объект-генератор с фиксированным начальным значением, но все же я решила, что это неплохой пример действия метода send.

Часть 4. Как использовать объекты с бесконечной генерацией чисел?

Во-первых, есть такая библиотека itertools, в которой много полезных функций для оперирования такого рода объектами.

В пример приведу одну из них islice. Функция islice принимает как минимум объект-генератор и значение, до которого нужно итерироваться. Возвращает конечный итератор.

from itertools import islice

def gener():
    i = 0
    while True:
        yield i
        i += 1

for i in islice(gener(), 5)
    print i,

0 1 2 3 4

Можно написать аналог такой функции, используя генератор.

def gener(*args):
    if args == []:
        step = 1
    else:
        step = args[0]
    i = yield
    while True:
        x = yield i
        if not x:
            i += step
        else:
            i = x

def slice(gen, start, stop, step):
    g = gen(step)
    g.send(None)
    x = g.send(start)
    yield x
    while True:
        x = g.next()
        if x < stop:
            yield x
        else:
            raise StopIteration

for i in slice(gener, 0, 10, 2):
    print i,

0 2 4 6 8

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

2 комментария:

  1. http://simeonvisser.com/posts/python-3-using-yield-from-in-generators-part-1.html

    Привет от Python 3.3 =))

    ОтветитьУдалить
  2. .next() уже не работает, теперь нужно писать __next__()

    ОтветитьУдалить