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