Некоторое время назад я писал о том, как можно добавить асинхронность в django. Теперь прошлый пост имеет только исторический интерес, если не читали, то не читайте. Принципиально подход не изменился, но стало гораздо проще и понятнее - не могу не написать. И пример кода другой.
Я писал только о django orm, и камнем преткновения там является обращение к базе данных, но подход можно обобщить на любой код, содержащий операции ввода-вывода.
В общем, выясняется, что добавить асинхронность в 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 можно ставить звёздочки
