На сей раз мы ещё немного усложним задачу.
Во-первых, хочется замаскировать отправку значений в сопрограмму так, чтобы это выглядело как простой вызов функции, а не вызов метода объекта.
Во-вторых, теперь перед добавлением записи нам нужно проверить, существует ли она. Если запись уже существует, то её не нужно добавлять.
Для решения первой задачи сначала мне пришло в голову такое решение - сделать дополнительный класс-обёртку с методом __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, с его периодическими изменениями и с разным поведением в разных версиях, вообще - не самый лучший язык для написания надёжных программ с долгим сроком поддержки.
Отправить комментарий