Эта заметка является идейным и фактическим продолжением одной из моих прошлых заметок - Генераторы и сопрограммы Python. На сей раз я продолжу борьбу за элегантность исходного кода, продолжая приносить в жертву лёгкость понимания средств, лежащих в основе этой элегантности :)
На сей раз мы ещё немного усложним задачу.
Во-первых, хочется замаскировать отправку значений в сопрограмму так, чтобы это выглядело как простой вызов функции, а не вызов метода объекта.
Во-вторых, теперь перед добавлением записи нам нужно проверить, существует ли она. Если запись уже существует, то её не нужно добавлять.
Для решения первой задачи сначала мне пришло в голову такое решение - сделать дополнительный класс-обёртку с методом __call__:
class wrapped_coro(): def __init__(self, coro): self.coro = coro def __call__(self, *args, **kwargs): return self.coro.send(*args, **kwargs) class wrapper(): def __init__(self, coro, *args, **kwargs): self.coro = coro(*args, **kwargs) def __enter__(self): self.coro.next() return wrapped_coro(self.coro) def __exit__(self, type, value, traceback): self.coro.close() if value is None: return True return False
Однако, немного подумав, я понял, что это ничем не оправданный оверинжениринг, удалил класс wrapped_coro и переписал wrapper вот так:
class wrapper(): def __init__(self, coro, *args, **kwargs): self.coro = coro(*args, **kwargs) def __enter__(self): self.coro.next() return self.coro.send def __exit__(self, type, value, traceback): self.coro.close() if value is None: return True return False
Получилось даже ещё проще, чем в прошлой заметке. Теперь функцию копирования можно переписать так:
def copy(db): """ Подпрогрмма, использующая генератор и сопрограмму для копирования содержимого таблицы user в таблицу user2 """ with wrapper(writer, db) as write: for row in reader(db): write(row)
Для решения второй задачи воспользуемся ещё одной возможностью, предоставляемой оператором yield - он может не только возвращать значение или только считывать его, но и считывать и возвращать одновременно. Делается это так:
def checker(db): """Сопрограмма. Проверяет, что указанный пользователь уже добавлен в таблицу users2""" select = db.cursor() exists = None try: while True: row = (yield exists) select.execute('''SELECT COUNT(*) FROM user2 WHERE surname = %s AND name = %s AND patronym = %s''', row) count, = select.fetchone() exists = count != 0 except GeneratorExit: select.close()
Перед началом использованием сопрограммы, как и прежде, нужно прокрутить её до первого оператора yield. Делается это, как и прежде, вызовом метода next из обёртки.
Новый вариант функции копирования примет следующий вид:
def copy(db): """ Подпрогрмма, использующая сопрограммы для дополнения таблицы user2 содержимым таблицы user """ with wrapper(writer, db) as write: with wrapper(checker, db) as check: for row in reader(db): if not check(row): write(row)
Читается, на мой взгляд, хорошо - код компактен и его логика легко просматривается, если понимать, для чего нужен wrapper. Полностью программа теперь будет выглядеть так:
#!/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 checker(db): """Сопрограмма. Проверяет, что указанный пользователь уже добавлен в таблицу users2""" select = db.cursor() exists = None try: while True: row = (yield exists) select.execute('''SELECT COUNT(*) FROM user2 WHERE surname = %s AND name = %s AND patronym = %s''', row) count, = select.fetchone() exists = count != 0 except GeneratorExit: select.close() class wrapper(): def __init__(self, coro, *args, **kwargs): self.coro = coro(*args, **kwargs) def __enter__(self): self.coro.next() return self.coro.send def __exit__(self, type, value, traceback): self.coro.close() if value is None: return True return False def copy(db): """ Подпрогрмма, использующая сопрограммы для дополнения таблицы user2 содержимым таблицы user """ with wrapper(writer, db) as write: with wrapper(checker, db) as check: for row in reader(db): if not check(row): write(row) db = MySQLdb.connect(user = 'user', passwd = 'p4ssw0rd', db = 'database', charset = 'UTF8') copy(db) db.close()