Здравствуйте, дорогие читатели! Сегодня — ещё одна статья из рубрики джангологии.
Раньше я уже писал о своих идеях (1 и 2) о том, как сделать django асинхронным. Они основывались, вслед за sqlalchemy, на использовании гринлетов. Несмотря на то, что proof-of-concept был успешно получен, а трудностей — встречено меньше, чем ожидалось, я всё-таки отказался от этого подхода. Во-первых, он уже применяется в sqlalchemy. Во-вторых, это ведёт к усложнению, и растёт так называемая test matrix — потому что поддерживается как синхронный случай, так и асинхронный. А simple, как мы знаем, is better than complex.
Так вот, я решил возобновить свои попытки, изменив подход на более радикальный. А именно, необратимо переписать django на async-only, сломав совместимость полностью. Для этого потребуется в половине функций заменить def на async def и добавить await при их вызове. Я уверен, что такой подход лучше.
Не то, чтобы асинхронный django был очень кому-нибудь нужен, особенно теперь, или от этого будет какой-то фантастический выигрыш в производительности — дело в том, что я хочу попробовать на практике агентное программирование, а это, как раз подходящий проект: есть чёткий план и много кода, который нужно менять.

Мысль о том, что просто переписать всё на async/await — совсем не глупая идея, приходила мне в голову, ещё когда я работал над proof-of-concept-ом версии с гринлетами. Если нет совместимости — нет и проблем. Вот, например, версия с гринлетами поддерживала синхронный режим, но она чуть-чуть отличалась от самой django, и нужно было решать, в какой степени оставить между ними совместимость. Теперь же я считаю, что самая прямолинейная идея — переписать всё, без задней мысли о совместимости, и есть самая лучшая.
Те, кто читал мои предыдущии посты, наверное, в курсе — тем не менее, остановлюсь на первом важном аспекте: django часто функционирует «ленивым» образом, делая запросы в базу по мере необходимости. Например, при попытке узнать длину кверисета или при чтении некоторых атрибутов у объекта. Так вот — такое поведение действительно не совместимо с async/await, и поддерживать его не планируется. А планируется сделать все запросы в базу явными.
Явные запросы в базу
Кверисеты останутся ленивыми, но, чтобы их вычислить, нужно будет написать слово await:
await (qs := Book.objects.all())print(len(qs))
Если не написать await, будет эксепшн. Второй способ, как можно вычислить кверисет — это асинхронная итерация:
qs = Book.objects.all()async for book in qs: print(book.title)
Всё остальное, в том числе, доступ к атрибутам, к запросам в базу приводить не должно.
Например, пусть у нас есть объект book и у неё — атрибут author, на который он ссылается по внешнему ключу (Foreign Key). В этом случае, book.author должен нам вернуть автора, если мы его уже достали из базы (с помощью select_related или prefetch_related) или выбросить ошибку, если мы этого не сделали.
Кстати, в новой версии django уже сделали шаг в этом направлении: там можно указать fetch_mode(RAISE). В этом случае, связанные объекты не будут подтягиваться из базы автоматически. Это и есть как раз то поведение, которое нам нужно для асинхронного django.

Что касается так называемых свойств (вроде book.author), соответствующих связанным объектам, тут возникает ещё другой вопрос: что делать, если мы знаем, что ещё не достали из базы book.author, и хотим явно это сделать (только для данного объекта book). Я считаю, для этого должен быть какой-то отдельный способ (не доступ к атрибуту). Я предлагаю такой:
await book.get('author')
Deepseek, кстати, такой подход хвалит (ну, работа у него такая):

Последовательное выполнение
Предыдущий пункт про ленивые запросы будет единственным существенное отличием этого асинхронного форка от django. Остальное поведение менять не придётся — благодаря тому, что я собираюсь оставить парадигму последовательного выполнения. Это означает, что, несмотря на асинхронность, все операции, в рамках обработки одного запроса, мы всё равно будем выполнять последовательно, одну за другой. То есть, точно так же, как в обычном django. Даже никаких «fire and forget» не будет: если запускаем какую-то операцию, дожидаемся её завершения.
Всё это близко идее про виртуальные потоки из Java и Project Loom. У нас будет тоже что-то вроде виртуальных потоков. Вот, например, в django есть принцип «1 соединение с базой на 1 поток» — у нас будет 1 соединение на 1 «виртуальный поток». При таком подходе, даже поддержка сигналов не станет какой-то проблемой — все хендлеры будут выполняться один за другим.
Стоит отметить, что async/await — это единственная вещь, которую я планирую добавлять в рамках этого форка. Новых фич добавлять не планируется.
Тестирование
Важный вопрос, — как это всё тестировать. Особенно если код будут писать агенты. Хорошо бы заюзать имеющиеся тесты django — их там несколько тысяч. И у меня есть план, как это сделать.
Во-первых, не обязательно сразу добавлять везде async и await. Первым делом, можно поменять парадигму с неявных запросов в базу на явные (как описано в п. 1) в имеющейся (блокирующей) версии. Потом — адаптировать тесты под это, убедиться, что они проходят.
Потом уже — добавить везде async/await. Но драйверы баз данных можно будет пока оставить прежние (блокирующие). В этом случае, реальной асинхронности не будет, и любую корутину можно будет выполнить, просто вызвав у неё next():
next(co.__await__())
То есть, например, можно будет написать декоратор pseudoasync, который будет превращать любую асинхронную функцию в обычную:
@pseudoasyncasync def mytest(): async for obj in Book.objects.all(): print(book.pk)
Потом — можно будет снова запустить джанговскую test suite и добиться, чтобы тесты прошли.
И уже в самом конце — тестировать с асинхронным драйвером.
Статус
Статус у проекта, конечно, самый лучший: я ещё ничего не начинал. Собираюсь использовать Copilot или Claude. Нужно будет учиться работать циклами и в режиме планирования. Кстати, если кто-то хочет поработать над этим странным проектом в режиме парного программирования — напишите в л/с.
Имя у проекта осталось с прошлого раза — это vinyl. В целом, deepseek говорит, что всё придумано достаточно хитро и имеет шансы на успех. А вы что думаете?
ссылка на оригинал статьи https://habr.com/ru/articles/1046604/