Пользователь
0,0
рейтинг
4 октября 2012 в 18:17

Разработка → Самая короткая запись асинхронных вызовов в tornado или патчим байткод в декораторе

Сложный асинхронный обработчик в tornado иногда расползается на десятки callback функций, из-за чего становится трудно воспринимать и модифицировать код. Поэтому существует модуль tornado.gen, позволяющий писать обработчик как генератор. Но много yield gen.Task(...) тоже выглядит не очень. Поэтому в порыве бреда я написал упрощающий запись декоратор:
До После
@asynchronous
@gen.engine
def get(self):
    result, status = yield gen.Task(
        db.users.find_one, {
            '_id': ObjectId(user_id),
        },
    )
@asynchronous
@gen.engine
@shortgen
def get(self):
    result, status << db.users.find_one_e({
        '_id': ObjectId(user_id),
        },
    )


Как это работает


Как вы уже заметили, мы заменили yield на <<. Так как python нам не позволит сделать это стандартными средствами, нам нужно модифицировать байткод. Для простой работы с ним воспользуемся модулем Byteplay. Посмотрим байткод двух простых функций:
from byteplay import Code
from pprint import pprint

def gen():
    a = yield 1
pprint(Code.from_code(gen.func_code).code)
[(SetLineno, 5),  # переходим на 5 строку
 (LOAD_CONST, 1), # загружаем константу 1
 (YIELD_VALUE, None), # "отдаём" загруженное значение
 (STORE_FAST, 'a'), # записываем в переменную a
 (LOAD_CONST, None),
 (RETURN_VALUE, None)]
def shift():
    a << 1
pprint(Code.from_code(shift.func_code).code)




[(SetLineno, 10),
 (LOAD_GLOBAL, 'a'),  # a из глобального пространства
 (LOAD_CONST, 1), # загружаем константу 1
 (BINARY_LSHIFT, None), # делаем сдвиг влево для a
 (POP_TOP, None),  # убираем верхний элемент стека
 (LOAD_CONST, None),
 (RETURN_VALUE, None)]

Поэтому сделаем простой патчер сугубо для этой ситуации:
from byteplay import YIELD_VALUE, STORE_FAST
code = Code.from_code(shift.func_code)
code.code[3] = (YIELD_VALUE, None)
code.code[4] = (STORE_FAST, 'a')
code.code.pop(1)
pprint(code.code)
[(SetLineno, 10), 
 (LOAD_CONST, 1),
 (YIELD_VALUE, None),
 (STORE_FAST, 'a'),
 (LOAD_CONST, None),
 (RETURN_VALUE, None)]

Теперь у нас есть байткод почти идентичный байткоду функции gen, применим его к shift и проверим результат:
shift.func_code = code.to_code()
res_gen = gen().send(None)
res_shift = shift().send(None)
print res_gen
print res_shift
print res_gen == res_shift
1
1
True




Результат получился одинаковым. Код для общей ситуации можно посмотреть на github. Про байткод подробнее можно узнать в официальной документации. А пока мы вернёмся к tornado. Возьмём уже готовый декоратор shortgen. И напишем простой обработчик:

def fetch(callback):
    callback(1)

class Handler(BaseHandler):
    @asynchronous
    @gen.engine
    @shortgen
    def get(self):
        result << gen.Task(fetch)

Код стал немного лучше, но нам всё равно приходится вручную оборачивать вызов в gen.Task, поэтому создадим ещё один декоратор для автоматизации этого процесса:

def fastgen(fnc):
    return partial(gen.Task, fnc)

@fastgen
def fetch(callback):
    callback(1)

class Handler(BaseHandler):
    @asynchronous
    @gen.engine
    @shortgen
    def get(self):
        result << fetch()

Теперь всё выглядит вполне прилично, но как это будет работать со сторонними библиотеками? А никак, поэтому теперь нам нужно пропатчить их! Нет, патчить байткод мы сейчас не будем, а применим просто monkey patch. Чтобы не сломать старый код, мы заменим __getattribute__ у нужных классов на:

def getattribute(self, name):
    attr = None
    if name.find('_e') == len(name) - 2:
        attr = getattr(self, name[:-2])
    if hasattr(attr, '__call__'):
        return fastgen(attr)
    else:
        return super(self.__class__, self).__getattribute__(name)

Теперь если у пропатченного объекта нет атрибута, например, find_e(постфикс _e добавлен чтобы не сломать старый код), нам вернётся атрибут find, обёрнутый в декоратор fasttgen.
И теперь код, например, для asyncmongo, будет выглядеть так:

from asyncmongo.cursor import Cursor
Cursor.__getattribute__ = getattribute
class Handler(BaseHandler):
    @asynchronous
    @gen.engine
    @shortgen
    def get(self):
        result, status << self.db.posts.find_e({'name': 'post'})

Как этим воспользоваться


Для начала установим получившийся модуль:

pip install -e git+https://github.com/nvbn/evilshortgen.git#egg=evilshortgen

Теперь пропатчим нужные нам классы:

from evilshortgen import shortpatch
shortpatch(Cls1, Cls2, Cls3)

Обернём в декоратор собственные асинхронные методы и функции:

from evilshortgen import fastgen
@fastgen
def fetch(id, callback):
    return callback(id)

И воспользуемся в обработчике:

from evilshortgen import shortgen
class Handler(BaseHandler):
    @asynchronous
    @gen.engine
    @shortgen
    def get(self, id):
        data << fetch(12)
        num, user << Cls1.fetch()

Известные проблемы


Вызов может устанавливать значение только переменным:

a << fetch()  # работает
self.a << fetch()  # не работает

Сложные распаковки не поддерживаются:

a, b << fetch()  # работает
(a, b), c << fetch()  # не работает

Ссылки


Evilshortgen на github
Подробно про байткод
Byteplay
Яковлев Владимир @nvbn
карма
73,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (18)

  • +2
    Как минимум, узнал про модуль tornado.gen, спасибо!
  • –1
    А txmongo, не пробовали случайно?
    Он ведь вроде бы как раз и предназначен для асинхронных запросов к монгодб.
    • 0
      Нет, пока asyncmongo хватает. Он тоже для этого.
      • 0
        О, пожалуй, то что нужно, спасибо
        • 0
          посмотрите еще motor обязательно.
          • 0
            Спасибо, посмотрю
  • +1
    Есть народ такой «Нахуа». Костыль ведь. При чём работающий не всегда. Или на творчество потянуло?
    • 0
      Это так, игрища =)
      • –1
        в pypy его засунуть — он будет производить мега-электричество мега-пользу
        • 0
          Это каким чудесным образом? Или вы написали чтобы написать?
          • 0
            nvbn засунуть, а не его творение
            вы ведь прочитали, чтобы спросить?
            • 0
              Ааа, теперь понял. Извините.
  • 0
    Такое лучше спрятать подальше и никому не показывать.
    • 0
      Да ладно, как информация «как можно делать, но не нужно» пойдёт =)
  • 0
    наверное все же «Самая короткаЯ»
  • +1
    <irony>Как только люди не извращаются, лишь бы Erlang не учить...</irony>
  • 0
    Очень по-хакерски =)

    Но, я уверен, тут нужно смотреть в сторону модификации абстрактного синтаксического дерева (AST), а не байт-кода. Для этого есть модуль docs.python.org/library/ast.html

    Допустим, если встретили в AST функцию, обернутую в декоратор с именем @shortgen, то ищем в её теле оператор бинарного сдвига и заменяем его на yield + Task.
    Из плюсов:
    * лучшая переносимость между версиями (т.к. формат AST документирован, а байт-код может изменяться без предупреждения)
    * трансформация происходит в момент «компиляции» а не в момент импорта (т.е. в .pyc файле будет уже yield).
    * код AST-трансформации по идее должен быть гораздо проще
    * не нужно ничего манкипатчить
    * проблемы со сложной декомпозицией и установкой результата «не-переменным» должны отпасть.
    Из минусов — я не знаю есть ли возможность применять AST трансформацию автоматически, скорее всего нужно писать Мakefile и компилировать .pyo отдельно. К сожалению, в Python нет опций компилятора, как в Erlang parse_transform например.
    Может попробуете на досуге? ;-)

    Но вообще очень интересный способ, спасибо! Как в NodeJS без yield живут не понимаю.
  • 0
    Сделал то же самое на AST, если кому интересно habrahabr.ru/post/153949/

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.