На сей раз мы ещё немного усложним задачу.
Во-первых, хочется замаскировать отправку значений в сопрограмму так, чтобы это выглядело как простой вызов функции, а не вызов метода объекта.
Во-вторых, теперь перед добавлением записи нам нужно проверить, существует ли она. Если запись уже существует, то её не нужно добавлять.
Для решения первой задачи сначала мне пришло в голову такое решение - сделать дополнительный класс-обёртку с методом __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()
2 комментария:
>> Во-первых, хочется замаскировать отправку значений в подпрограмму так,
>> чтобы это выглядело как простой вызов функции, а не вызов метода объекта.
Это плохая затея, особенно если планируется использовать подпрограмму в других программах: что-то мне подсказывает, что кто-то на этом огребёт трудновылавливаемых проблем. Причём там, где их меньше всего ждёшь.
>> Читается, на мой взгляд, хорошо - код компактен и его логика легко просматривается,
>> ___если понимать, для чего нужен wrapper___.
Последняя фраза выдаёт ковбоя с головой :-) Кстати, в тексте wrapper не документирован ни разу.
Пример: не далее как нынче утром автор этих строк правил один алгоритм и портировал его на С. Всё ОК, но пару дней назад автор этих строк поправил некий параметр nmax и сделал его nmax=1 (это число итераций). И забыл, естественно, после бурных выходных. А теперь алгоритм расходится с эталонным алгоритмом, причём капитально так расходится. Автору потребовалось добрых полчаса на выяснение этого факапа, а ведь алгоритм состоит из 300 строк кода, и "поправлен" неделю назад.
Это я к чему: компилятор с кодом разберётся, конечно, но не факт, что он его интерпретирует так, как задумывалось автором.
>Это плохая затея, особенно если планируется использовать подпрограмму в других программах: что-то мне подсказывает, что кто-то на этом огребёт трудновылавливаемых проблем. Причём там, где их меньше всего ждёшь.
Этот объект является представлением сопрограммы. Согласитесь, несколько странно, если мы описываем сопрограмму, а потом пользуемся ей, как объектом. Поэтому именно в этом случае маскировка оправдана.
>Последняя фраза выдаёт ковбоя с головой :-) Кстати, в тексте wrapper не документирован ни разу.
Это учебный пример, поэтому wrapper так незатейливо назван и описан только в самом тексте статьи. Wrapper - это обёртка, которая позволяет использовать сопрограмму как менеджер контекста в операторе with. Код его быстрее прочитать, чем описывать.
Лучше быть ковбоем, чем стадом мычащих коров :)
>Это я к чему: компилятор с кодом разберётся, конечно, но не факт, что он его интерпретирует так, как задумывалось автором.
Нужно привыкнуть к этому, как к данности. Если хотите, чтобы программа не разваливалась при каждом изменении - покройте её тестами, а каждый тест продокументируйте. Без тестов вообще нет никаких гарантий того, что написанное делает то, что ожидается. Без них вообще зачастую проще будет переписать программу заново, чем что-то пытаться в ней изменять.
Все языки программирования разные, имеют множество не очевидных нюансов, которые ещё и меняются со временем. Программисты тоже мыслят по-разному и пользуются разными приёмами. Тут вообще сложно что-то гарантировать. Есть громкие примеры, как с оптимизированной версией memmove в glibc или системным вызовом close, возвращающим ошибку. Python, с его периодическими изменениями и с разным поведением в разных версиях, вообще - не самый лучший язык для написания надёжных программ с долгим сроком поддержки.
Отправить комментарий