Поздравляю с годом Кролика и желаю сбычи ваших мечт! Хочу вас обрадовать, что, судя по всему, в новом году будет продолжение франшизы про асинхронный django. Версия на гринлетах скоро получит новую, более изящную форму. И новое название - fibers (старое, greenhack, никуда не годилось).
Вот как это выглядит:
async def myview(request):
async with fibers:
# Здесь можно использовать django
obj = MyObject.objects.get()
print(obj.related_obj)
# Здесь можно писать асинхронный код
await notify_somebody(request)
Теперь, использовать django внутри асинхронной функции можно под (асинхронным) контекстным менеджером. По-моему, юзабилити сильно выигрывает. И семантически - тоже понятнее, что происходит, нет?
Пару слов - для тех, кто не читал мои предыдущие статьи. Несмотря на то, что используется django, и код похож на блокирующий - это не так, блокирующих вызовов нет. Всю магию обеспечивают гринлеты. Похожий способ использует sqlalchemy.
Так вот, пример c кодом неплохо смотрится, правда? Вы спросите - почему было сразу так не сделать? Дело в том, что мне только недавно пришло в голову, что это технически возможно. В том случае, если вспомнить, как реализованы корутины в питоне (это генераторы), и воспользоваться этой особенностью реализации. Для нетерпеливых - вот ссылка на gist с кодом.
Как же можно воспользоваться тем, что корутины - это генераторы? Приведу простой пример:
async def hi():
print('hi')
Несмотря на то, что перед нами асинхронная функция, не обязательно иметь event loop, чтобы запустить её. Можно сделать так:
try:
gen = hi()
gen.send(None)
except StopIteration:
pass
Будет напечатано hi
.
У контекстного менеджера fibers - такой же принцип. Он устроен так, что весь код внутри него можно выполнить вызовом gen.send() (вот эта строчка). Дальше, мы оборачиваем этот вызов в гринлет - и применяем наш обычный подход.
Вы знаете, как выглядит простейший Awaitable? Вот один из вариантов, не самый типичный:
class Simple:
def __await__(self):
return 'hi'
yield
await Simple()
вернёт'hi'
.
__await__
- это генератор. В отличие от корутин, он может делать как yield, так и yield from. Тогда как корутины могут делать только await, что соответствует yield from.
Контекстный менеджер fibers тоже использует кастомные Awaitable. Они делают yield специальных значений start и end. Event loop их не поймёт. Поэтому нам нужно обернуть какую-нибудь корутину (например, верхнего уровня), чтобы она не пропускала их наружу. Сделать это относительно несложно: например, если у Вас - ASGI приложение, то можно обернуть это приложение целиком.
Иллюстрацию подхода можно посмотреть в gist. По сути, это просто ещё один "фронтенд" к библиотеке greenhack: для него даже не потребовалось вносить в неё изменения. Удобный API для того же самого, больше ничего.
Когда всё будет готово? Всё упирается в асинхронный бэкенд для django. Он готов, но не протестирован: нужно пройти testsuite для бэкендов django. Там около 70 тестов - когда они станут проходить, проект будет готов к продакшну. Обещать по срокам ничего не буду, но буду писать недельные отчёты (нет, не на хабре - на гитхабе). К сожалению, рабочие проекты не связаны с этой темой.
Кстати, названием проект обязан языку Ruby, точнее, Ruby Fibers. Дело в том, что в Ruby нет async/await, для асинхронности там в принципе не используются "функции другого цвета". Так достигается совместимость блокирующего кода с асинхронным - от которой в питоне отказались. К лучшему или нет - я не берусь сказать. Но я знаю, как это "исправить" - для тех случаев, когда эта совместимость нужна.