Некоторое время назад я писал о том, как можно добавить асинхронность в 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)


  1. Zada
    02.12.2021 04:25
    +1

    Почему бы не оформить пулл-реквест к основному репо и не послушать, что вам скажут там?


    1. abetkin Автор
      02.12.2021 07:57
      -2

      я знаю и про менее наивные способы, но пока эстафета у хабра-читателей)


  1. 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 является ограничивающим фактором.

    Без подобной задачи проверить полезность твоего решения не предоставляется возможным. То, что это заработает само по себе - сомнения не вызывает.

    Успехов в разработке, будет интересно посмотреть проверку концепта на реальном примере.