Некоторое время назад я писал о том, как можно добавить асинхронность в django. Теперь прошлый пост имеет только исторический интерес, если не читали, то и не читайте (но он тут).
Я писал только о django orm, и камнем преткновения там является обращение к базе данных, но подход можно обобщить на любой код, содержащий операции ввода-вывода.
Что мы хотим получить: вообще, нативную поддержку асинхронности, в любом варианте, а в этой статье - чтобы можно было использовать весь API django в асинхронном контексте, ничего не меняя. Например, делать вызовы await
MyModel.objects.get()
В общем, выясняется, что добавить асинхронность в django - по сути, не сложнее, чем портировать его в асинхронность - раз и навсегда. Вы примерно представляете, как бы это нужно было делать: берём асинхронный драйвер с API примерно похожим на драйверы django и заменяем в недрах кода вызовы вроде driver.execute_sql(sql, params)
на вызовы вроде await async_driver.execute_sql(sql, params)
. Конечно, мы сразу получим SyntaxError
потому что нельзя использовать await в обычной функции, и будем вынуждены добавить async и await во всём стеке вызовов.
Весь API django отлично применим к асинхронной версии, за небольшими адаптациями. Например, ленивых атрибутов больше не будет, а обращение к такому атрибуту будет возможно, если вы заранее извлекли его из базы, например, с помощью prefetch_related или select_related (для обращения к атрибуту, который требует обращения к базе, нужен другой API).
В принципе, работа несложная и даже относительно небольшая. Так вот, моя точка зрения, что добавление асинхронности (не ломая старый, синхронный, API) - это так же просто - возможно, за небольшими накладками.
В общем, есть в питоне такая вещь, как yield from, которая одинаково применима как в синхронном, так и в асинхронном контексте. Под капотом, разумеется, ничего не меняющая. На мой взгляд, её и нужно использовать для таких проектов, которые хотят поддержать одновременно синхронную и асинхронную версию. Даже для тех, которые пишутся с нуля - ну как мне кажется. А уж для легаси - вообще без разговоров.
ПУЛ-РЕКВЕСТ - ВОТ ОН
Итак, это пул-реквест - в ветку main django (между прочим, пул из ветки main ещё ни разу не приводил к конфликтам). В нём, конечно, поддерживается только малая часть API django - конечно, потому что другие цели не ставились.
Что мы в нём видим? Конечно, множество вызовов yield from перед функциями - как я обещал.
SyncDriver и AsyncDriver на самом деле не драйверы, а классы со вспомогательными функциями. Переименовывать лень, да и не очень понятно, как назвать. Как видите, они реализуют функцию execute(operations)
Дело в том, что мы превратили наш код в генератор, и эти самые 2 класса, которые не драйверы, запрашивают у него "операции", по одной, которые нужно исполнить: функции, как они есть, с параметрами.
Где же сами драйверы? Это тоже достаточно интересно. Вот где:
class SQLCompiler:
a = Asynchrony()
@a.branch()
def execute_sql(self, result_type=MULTI, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE):
...
@a.branch()
async def execute_sql(self, result_type=MULTI, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE):
...
Как Вы поняли, это 2 ветки одной и той же функции - синхронная и асинхронная, которые и пользуются драйвером. Вот так всё просто, и таких веток можно иметь сколько угодно. Кстати, asyncpg ни капли не совместим с dbapi - я уверен, без всяких разумных причин.
Всё остальное не так интересно. Вот так, например, я сделал кверисеты Awaitable
def __await__(self):
return self.__iter__().__await__()
Или например, у нас многие функции содержат yield from - что же делать, если у нас функция сама является генератором (в django часто используется yield для создания итераторов). Я нашёл простое решение: поместим конструкции yield внутрь другой функции (в моём случае - def iterator
, и они не будут конфликтовать).
Ещё вы можете увидеть декоратор @consume_generator
. Он превращает генератор в обычную функцию (которая возможно вернёт корутину). Предназначено для вызова извне (читай, пользователем). В общем, любопытные всё увидят.
Это всё. Репозиторию pwtail/django можно ставить звёздочки
Комментарии (3)
danilovmy
04.12.2021 13:50Привет. Слежу за ходом мысли в репозитории и в статьях.
Вопрос первый.
Метод get. Вычисление Len. Ты делаешь yield from из числа, которое получил в функции _len _. После ты делаешь Len ==1.
Если сравнить Генератор и число то это точно false даже если в генераторе один шаг. Итерация из числа - тоже выглядит как ошибка.
Вопрос второй
__ await__ у тебя в функции просто ожидание функции итер. Так и напиши await iter(self), ты же используешь дандлеры, делая код менее очевидным.
Третье.
Ты в статье говоришь о прослойке базы и независимости от Джанго и сам же захардкоживаешь постгресовый драйвер. Т.е если я захочу твой концепт проверить с другой базой - надо будет код править.
Четвертое
В некоторых случаях тебе не нужен явный генератор, поскольку он навешен поверх неявного например list((yield from self._iterable_class(self)))
Поскольку лист сам ждёт итератор, то достаточно list(*self._iterable_class(self)), хотя подозреваю, что там должен быть list(*self)
Завершая:
Написать синхронный код и написать асинхронный код это две разных задачи. У тебя добавлены интерфейсы для использования orm в асинхронном коде. Но сам код у тебя все равно работает последовательно. На примере get: получаем все объекты проверяем длину, выдаём первый, если он единственный. Инициализация объектов идёт последовательно. Для правильно написанного быстрого запроса и кривонаписанного долгого инита объектов весь твой асинхронный код будет ждать последовательного появления объектов созданных на базе асинхронно полученных данных по всем объектам. А раз так - нужно реализовать ожидаемый инит объекта, потому, как блокирующим станет именно __ init ___.
Однако верность постановки задачи вызывает у меня сильное сомнение. Я вижу, что ты хочешь встроить ожидаемость в орм. Даже не сомневаюсь, что при достаточном терпении тебе удастся это сделать без изменения логики работы. Проверять твой концепт надо на тех задачах, в которых последовательность в работе orm является ограничивающим фактором.
Без подобной задачи проверить полезность твоего решения не предоставляется возможным. То, что это заработает само по себе - сомнения не вызывает.
Успехов в разработке, будет интересно посмотреть проверку концепта на реальном примере.
Zada
Почему бы не оформить пулл-реквест к основному репо и не послушать, что вам скажут там?
abetkin Автор
я знаю и про менее наивные способы, но пока эстафета у хабра-читателей)