воскресенье, 6 января 2013 г.

Генераторы и сопрограммы Python

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

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

Недоумевающим знатокам MySQL и прочих баз данных я могу сказать, что я осведомлён о существовании таких запросов как INSERT IGNORE ..., INSERT INTO ... ON DUPLICATE KEY UPDATE ..., REPLACE INTO ... и INSERT ... SELECT ..., знаю и о том, что можно написать хранимую процедуру или триггер. Всё это может существенно облегчить написание подобных программ, если речь идёт об обработке данных, имеющихся в самой базе данных. Однако данные могут быть не только в базе данных, они могут располагаться в файле, могут быть получены в результате опроса оборудования (ICMP или SNMP) или запроса к веб-сайту. В результате, часто приходится писать заново довольно-таки типовой код, реализующий всю это обработку данных.

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

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

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

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

И то и другое можно реализовать с помощью объектов, но при проектировании и реализации объекта придётся в явном виде выделить состояние объекта и его методы, а также реализовать логику, которая будет учитывать текущее состояние объекта для определения способа поведения каждого из методов. Также, зачастую, перед использованием объекта, его бывает нужно настроить, а после использования - правильно удалить. Конечно, объекты более универсальны, однако для решения простых задач они могут показаться избыточными и неуклюжими.

Итак, чтобы не быть голословным, приведу простой пример, копирующий записи из одной таблицы базы данных в другую. Такую задачу можно решить одним запросом INSERT ... SELECT ..., но вспомните всё то, что я написал выше, и учтите, что это лишь пример.
#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

def reader(db):
    """Генератор. Читает строки таблицы user"""
    select = db.cursor()
    select.execute('SELECT surname, name, patronym FROM user')
    for row in select:
        yield row
    select.close()

def writer(db):
    """Сопрограмма. Пишет строки в таблицу user2"""
    insert = db.cursor()
    try:
        while True:
            row = (yield)
            try:
                insert.execute('INSERT INTO user2(surname, name, patronym) VALUES(%s, %s, %s)', row)
                db.commit()
            except:
                db.rollback()
    except GeneratorExit:
        insert.close()

def copy(db):
    """
    Подпрогрмма, использующая генератор и сопрограмму для копирования
    содержимого таблицы user в таблицу user2
    """
    write = writer(db)
    write.next()

    for row in reader(db):
        write.send(row)

    write.close()

db = MySQLdb.connect(user = 'user',
                     passwd = 'p4ssw0rd',
                     db = 'database',
                     charset = 'UTF8')

copy(db)

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

Генераторы легко писать и использовать. Тем, кто знаком с генератором xrange, не составит труда понять, как работает генератор reader из примера.

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

В python есть интересный оператор with, который, как мне показалось, хорошо подошёл бы в этой ситуации для того, чтобы упростить использование сопрограмм. К сожалению, сопрограммы не реализуют интерфейс, необходимый для их использования в этом операторе. Однако, ничто не мешает нам реализовать обёртку, которая позволит использовать сопрограммы вместе с оператором with.

Класс-обёртка, который можно использоваться для произвольных сопрограмм, не только для writer из примера:
class wrapper():
    def __init__(self, coro, *args, **kwargs):
        self.coro = coro(*args, **kwargs)

    def __enter__(self):
        self.coro.next()
        return self.coro

    def __exit__(self, type, value, traceback):
        self.coro.close()
        if value is None:
            return True
        return False

    def send(self, *args, **kwargs):
        return self.coro.send(*args, **kwargs)
Переработанная функция копирования, использующая класс-обёртку и оператор with:
def copy(db):
    """
    Подпрогрмма, использующая генератор и сопрограмму для копирования
    содержимого таблицы user в таблицу user2
    """
    with wrapper(writer, db) as write:
        for row in reader(db):
            write.send(row)
Вот такой вариант мне нравится гораздо больше - все подготовительные и "уборочные" операции выполняются незаметно для пользователя. Может быть этот код сыроват, я ещё не волшеб опытный программист на python'е, я только учусь, однако он работает. Если вы знаете, как сделать лучше - милости прошу в комментарии.

На самом деле, сопрограммы можно использовать не только таким тривиальным образом. Можно составлять цепочки из сопрограмм, сообщая каждой из них, в какую следующую сопрограмму можно передавать данные. Генераторы тоже можно составлять в цепочки, так что последний генератор будет использовать данные из предыдущего. В сопрограмме можно передавать данные на обработку сразу нескольким сопрограммам, а несколько сопрограмм могут передавать данные на обработку одной, разветвляя и собирая поток обработки данных. Примеры есть в книге и на сайте Дэвида Бизли. На сайте и в презентации можно даже найти макет кооперативной многозадачной операционной системы, основанной на сопрограммах.

Бизли предостерегает от комбинирования сопрограмм и генераторов и приводит три типовых случая, когда оператор yield можно использовать без особых опасений: генераторы, потребители и кооперативная многозадачность.

Ссылки:
1. Дэвид Бизли. Python. Подробный справочник, 4-е издание, стр. 40-42, 126-128.
2. Страница на сайте Дэвида Бизли, посвящённая сопрограммам
3. Презентация Дэвида Бизли о сопрограммах

Комментариев нет: