Pull to refresh

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

Reading time3 min
Views78K
Предлагаю обсудить такую интересную, но мало используемую возможность python, как сопрограммы (coroutines).
Сопрограммы в питоне основаны на генераторах (ими, они, собственно и являются).
Поэтому, предлагаю начать именно с генераторов, в общем понимании. А потом разберём как написать свою сопрограмму.

Генераторы


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

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

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) чтением, подразумевая, что мы не делаем «работу» без необходимости.

В общем случае, работа с генераторами выглядит следующим образом:

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, как и с любым итерируемым оъектом. 


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

uniq = []
for line in lines_generator:
  if line not in uniq:
      uniq.append(line)


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

In [92]: gen = (x for x in xrange(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) в памяти, возвращаемое значение «вычисляется» каждый раз при обращении.

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


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

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

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

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() # Закрывем генератор


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

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

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)


На этом примере можно понять как писать свои более сложные (и полезные) сопрограммы.

Заключение.


Хоть проблемы, которые можно решить этим инструментом затрагивают очень многие области (такие как асинхронное программирование), многие разработчики предпочитают более привычные инструменты ООП. Но при этом сопрограммы могут быть очень полезным инструментом в вашем арсенале, поскольку они достаточно наглядны, а создание фунций более дешёвая операция по сравнению с созданием объекта класса.

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

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

UPD:
Исправил в примере короткой записи генератора range на xrange.
В 2-й версии python range() создаёт весь список сразу, для создания генератора надо использовать xrange(), в 3-й версии range == xrange (т.е. возвращает генератор).
Tags:
Hubs:
Total votes 61: ↑53 and ↓8+45
Comments27

Articles