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

Генераторы и сопрограммы Python. Часть 2

Эта заметка является идейным и фактическим продолжением одной из моих прошлых заметок - Генераторы и сопрограммы 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()

2 комментария:

virens комментирует...

>> Во-первых, хочется замаскировать отправку значений в подпрограмму так,
>> чтобы это выглядело как простой вызов функции, а не вызов метода объекта.

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

>> Читается, на мой взгляд, хорошо - код компактен и его логика легко просматривается,
>> ___если понимать, для чего нужен wrapper___.

Последняя фраза выдаёт ковбоя с головой :-) Кстати, в тексте wrapper не документирован ни разу.

Пример: не далее как нынче утром автор этих строк правил один алгоритм и портировал его на С. Всё ОК, но пару дней назад автор этих строк поправил некий параметр nmax и сделал его nmax=1 (это число итераций). И забыл, естественно, после бурных выходных. А теперь алгоритм расходится с эталонным алгоритмом, причём капитально так расходится. Автору потребовалось добрых полчаса на выяснение этого факапа, а ведь алгоритм состоит из 300 строк кода, и "поправлен" неделю назад.

Это я к чему: компилятор с кодом разберётся, конечно, но не факт, что он его интерпретирует так, как задумывалось автором.

morbo комментирует...

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

Этот объект является представлением сопрограммы. Согласитесь, несколько странно, если мы описываем сопрограмму, а потом пользуемся ей, как объектом. Поэтому именно в этом случае маскировка оправдана.

>Последняя фраза выдаёт ковбоя с головой :-) Кстати, в тексте wrapper не документирован ни разу.

Это учебный пример, поэтому wrapper так незатейливо назван и описан только в самом тексте статьи. Wrapper - это обёртка, которая позволяет использовать сопрограмму как менеджер контекста в операторе with. Код его быстрее прочитать, чем описывать.

Лучше быть ковбоем, чем стадом мычащих коров :)

>Это я к чему: компилятор с кодом разберётся, конечно, но не факт, что он его интерпретирует так, как задумывалось автором.

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

Все языки программирования разные, имеют множество не очевидных нюансов, которые ещё и меняются со временем. Программисты тоже мыслят по-разному и пользуются разными приёмами. Тут вообще сложно что-то гарантировать. Есть громкие примеры, как с оптимизированной версией memmove в glibc или системным вызовом close, возвращающим ошибку. Python, с его периодическими изменениями и с разным поведением в разных версиях, вообще - не самый лучший язык для написания надёжных программ с долгим сроком поддержки.