Сопрограммы в Python

Генераторы

Любой более-менее приличный програмист на Python значет, что есть в питоне такая замечательная штука, как функции-генераторы. Главная их особенность – это сохранение состояния между вызовами.

Напомню, как это выглядит.

Возьмём вот такую функцию:

1
2
3
4
5
6
7
def read_file_line_by_line(file_name):
  with open(file_name, 'r') as f:
      while True:
        line = f.readline()
        if not line:
          break
        yield line

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

Такой приём называют ленивым (lazy) чтением, подразумевая, что мы не делаем работу без необходимости.

Получаем генератор:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [78]: lines_generator = read_file_line_by_line("data.csv")
In [79]: type(lines_generator)
Out[79]: generator
In [83]: lines_generator.next()
Out[83]: 'time,host,event\n'
In [84]: lines_generator.next()
Out[84]: '1374039728,localhost,reboot\n'
In [85]: lines_generator.next()
Out[85]: '1374039730,localhost,start\n'
In [86]: lines_generator.next()

---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-86-65df1a2cb71b> in <module>()

----> 1 lines_generator.next()

StopIteration:

# Соответственно у меня в файле только 3 строчки 
# Как только читать больше нечего, возникает StopIteration эксепшн, как и с любым итерируемым оъектом. 

Естественно, чаще мы читаем значения из генератора в цикле, а не построчно:

1
2
3
4
5
uniq = []
for line in lines_generator:
  if line not in uniq:
      uniq.append(line)
# Пример надуманный, вероятно для таких выкрутасов лучше всё в set добавить, но это не имеет значения сейчас

Возможна короткая записть генератора:

1
2
3
4
5
6
7
8
9
10
11
In [92]: gen = (x for x in range(0, 100*10000))
In [93]: gen.next()
Out[93]: 0
In [94]: gen.next()
Out[94]: 1
In [95]: gen.next()
Out[95]: 2
In [96]: gen.next()
Out[96]: 3
In [97]: gen.next()
Out[97]: 4

Соответственно нам не надо загружать в память весь список range(0, 100*10000), возвращаемое значение “вычисляется” каждый раз при обращении.

Внимание, это не то же самое что списковые выражения!

Они возвращают весь список целиком сразу.

1
2
3
4
In [104]: type([x for x in range(0, 10)])
Out[104]: list
In [105]: type((x for x in range(0, 10)))
Out[105]: generator

Сопрограммы как частный случай генераторов

А теперь о том, ради чего это, собственно, затевалось. Оказывается, генератор может не только возвращать значения, но и принимать их на вход.

О стандарте можно почитать тут PEP 342.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def calc():
    history = []
    while True:
        x, y = (yield)
        if x == 'h':
            print history
            continue
        result = x + y
        print result
        history.append(result)

c = calc()

print type(c) # <type 'generator'>

c.next() # Необходимая инициация. Можно написать c.send(None)
c.send((1,2)) # Выведет 3
c.send((100, 30)) # Выведет 130
c.send((666, 0)) # Выведет 666
c.send(('h',0)) # Выведет [3, 130, 666]
c.close() # Закрывем генератор

Т.е. мы создали генератор, проинициализировали его и подаём ему входные данные.

Он, в свою очередь, эти данные обрабатывает и сохраняет своё состояние между вызовами до тех пор пока мы его не закрыли. После каждого вызова генератор возвращает управление туда, откуда его вызвали.

Тут бы следовало что-то рассказать о конечных автоматах, но, вероятно, я попробую написать об этом отдельно.

Так, с тем, как это работает, вроде, разобрались.

Давайте теперь избавим себя от необходимости каждый раз руками инициализировать генератор.

Сделать это можно примерно так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def coroutine(f):
    def wrap(*args,**kwargs):
        gen = f(*args,**kwargs)
        gen.send(None)
        return gen
    return wrap

@coroutine
def calc():
    history = []
    while True:
        x, y = (yield)
        if x == 'h':
            print history
            continue
        result = x + y
        print result
        history.append(result)

Тут всё понятно, я думаю.

Сопрограммы могут быть очень полезным инструментом в вашем арсенале разработчика, поскольку они достаточно наглядны, и при этом создание фунций более дешёвая операция по сравнению с созданием объекта класса.

Да и определённый академический интерес они представляют, как мне кажется.

Вот такая вот первая статья.

Опечатки, ошибки, замечания и пожелания можно присылать на shoonoise@gmal.com