Сложный асинхронный обработчик в tornado иногда расползается на десятки callback функций, из-за чего становится трудно воспринимать и модифицировать код. Поэтому существует модуль tornado.gen, позволяющий писать обработчик как генератор. Но много yield gen.Task(...) тоже выглядит не очень. Поэтому в порыве бреда я написал упрощающий запись декоратор:
Как вы уже заметили, мы заменили yield на <<. Так как python нам не позволит сделать это стандартными средствами, нам нужно модифицировать байткод. Для простой работы с ним воспользуемся модулем Byteplay. Посмотрим байткод двух простых функций:
Поэтому сделаем простой патчер сугубо для этой ситуации:
Теперь у нас есть байткод почти идентичный байткоду функции gen, применим его к shift и проверим результат:
Результат получился одинаковым. Код для общей ситуации можно посмотреть на github. Про байткод подробнее можно узнать в официальной документации. А пока мы вернёмся к tornado. Возьмём уже готовый декоратор shortgen. И напишем простой обработчик:
Код стал немного лучше, но нам всё равно приходится вручную оборачивать вызов в gen.Task, поэтому создадим ещё один декоратор для автоматизации этого процесса:
Теперь всё выглядит вполне прилично, но как это будет работать со сторонними библиотеками? А никак, поэтому теперь нам нужно пропатчить их! Нет, патчить байткод мы сейчас не будем, а применим просто monkey patch. Чтобы не сломать старый код, мы заменим __getattribute__ у нужных классов на:
Теперь если у пропатченного объекта нет атрибута, например, find_e(постфикс _e добавлен чтобы не сломать старый код), нам вернётся атрибут find, обёрнутый в декоратор fasttgen.
И теперь код, например, для asyncmongo, будет выглядеть так:
Для начала установим получившийся модуль:
Теперь пропатчим нужные нам классы:
Обернём в декоратор собственные асинхронные методы и функции:
И воспользуемся в обработчике:
Вызов может устанавливать значение только переменным:
Сложные распаковки не поддерживаются:
Evilshortgen на github
Подробно про байткод
Byteplay
| До | После |
|---|---|
|
|
Как это работает
Как вы уже заметили, мы заменили yield на <<. Так как python нам не позволит сделать это стандартными средствами, нам нужно модифицировать байткод. Для простой работы с ним воспользуемся модулем Byteplay. Посмотрим байткод двух простых функций:
|
|
|
|
Поэтому сделаем простой патчер сугубо для этой ситуации:
|
|
Теперь у нас есть байткод почти идентичный байткоду функции gen, применим его к shift и проверим результат:
|
|
Результат получился одинаковым. Код для общей ситуации можно посмотреть на 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