Перевод статьи: Jace Medlin — Async Views in Django 3.1
Написание асинхронного кода дает возможность значительно ускорить работу приложения. Django 3.1 наконец-то поддерживает асинхронные представления, middleware и тесты, и теперь самое время попробовать их в действие.
В этом посте я расскажу, как начать работу с новыми асинхронными представлениями Django.
Если вам интересно узнать больше о силе асинхронного кода, а также о различиях между потоками, многопроцессорностью и асинхронностью в Python, ознакомьтесь с моей публикацией «Speeding Up Python with Concurrency, Parallelism, and asyncio».
К концу этого поста вы сможете:
Вы также сможете ответить на следующие вопросы:
Если вы уже знакомы с самим Django, добавление асинхронной функциональности в представления чрезвычайно просто.
ASGI означает интерфейс шлюза асинхронного сервера (Asynchronous Server Gateway Interface). Это современное асинхронное продолжение WSGI, обеспечивающее стандарт для создания асинхронных веб-приложений на основе Python.
Также стоит упомянуть, что ASGI обратно совместим с WSGI, что упрощает переход с сервера WSGI, такого как Gunicorn или uWSGI, на сервер ASGI, такой как Uvicorn или Daphne, даже если вы не готовы переключиться на написание асинхронных приложений прямо сейчас.
Создайте новый каталог проекта вместе с новым проектом Django:
$ mkdir django-async-views && cd django-async-views $ python3.8 -m venv env $ source env/bin/activate (env)$ pip install django (env)$ django-admin.py startproject hello_async .
Не стесняйтесь использовать вместо virtualenv и Pip что то типа Poetry или Pipenv.
Django будет запускать ваши асинхронные представления, даже если вы используете встроенный сервер разработки, но на самом деле они не будут работать асинхронно, поэтому мы будем использовать Uvicorn для асинхронной работы.
Установите его:
(env)$ pip install uvicorn
Чтобы запустить свой проект с помощью Uvicorn, используйте следующую команду из корня вашего проекта:
uvicorn {name of your project}.asgi:application
В нашем случае это будет:
(env)$ uvicorn hello_async.asgi:application
Затем давайте создадим наше первое асинхронное представление. Добавьте новый файл для хранения ваших представлений в папке hello_async, а затем добавьте следующее представление:
# hello_async/views.py from django.http import HttpResponse async def index(request): return HttpResponse("Hello, async Django!")
Создать асинхронные представления в Django так же просто, как создать синхронное представление — все, что вам нужно сделать, это добавить ключевое слово async.
Обновите URL-адреса:
# hello_async/urls.py from django.contrib import admin from django.urls import path from hello_async.views import index urlpatterns = [ path("admin/", admin.site.urls), path("", index), ]
Теперь в терминале в корневой папке запустите:
(env)$ uvicorn hello_async.asgi:application --reload
Флаг —reload указывает uvicorn следить за вашими файлами на предмет изменений и перезагружать их, если они обнаруживаются. Это, вероятно, не требует пояснений.
Откройте http://localhost:8000/ в своем любимом веб-браузере:
Hello, async Django!
Не самое захватывающее в мире, но, эй, это только начало. Стоит отметить, что запуск этого представления со встроенным сервером разработки Django приведет к точно таким же функциональным возможностям и результатам. Это потому, что мы фактически не делаем ничего асинхронного в обработчике.
Стоит отметить, что поддержка async полностью обратно совместима, поэтому вы можете смешивать представления async и sync, middleware и тесты. Django выполнит каждый из них в соответствующем контексте выполнения.
Чтобы продемонстрировать это, добавьте несколько новых представлений:
# hello_async/views.py import asyncio from time import sleep import httpx from django.http import HttpResponse # helpers async def http_call_async(): for num in range(1, 6): await asyncio.sleep(1) print(num) async with httpx.AsyncClient() as client: r = await client.get("https://httpbin.org/") print(r) def http_call_sync(): for num in range(1, 6): sleep(1) print(num) r = httpx.get("https://httpbin.org/") print(r) # views async def index(request): return HttpResponse("Hello, async Django!") async def async_view(request): loop = asyncio.get_event_loop() loop.create_task(http_call_async()) return HttpResponse("Non-blocking HTTP request") def sync_view(request): http_call_sync() return HttpResponse("Blocking HTTP request")
Обновите URL-адреса:
# hello_async/urls.py from django.contrib import admin from django.urls import path from hello_async.views import index, async_view, sync_view urlpatterns = [ path("admin/", admin.site.urls), path("async/", async_view), path("sync/", sync_view), path("", index), ]
Установим HTTPX:
(env)$ pip install httpx
На работающем сервере перейдите по адресу http://localhost:8000/async/. Вы должны сразу увидеть ответ:
Non-blocking HTTP request
В вашем терминале вы должны увидеть:
INFO: 127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK 1 2 3 4 5 <Response [200 OK]>
Здесь ответ HTTP отправляется обратно перед первым вызовом sleep.
Затем перейдите по адресу http://localhost:8000/sync/. Получение ответа должно занять около пяти секунд:
Blocking HTTP request
Обратимся к терминалу:
1 2 3 4 5 <Response [200 OK]> INFO: 127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK
Здесь ответ HTTP отправляется после цикла, а запрос на https://httpbin.org/ завершается.
Теперь давайте напишем представление, которое запускает простую задачу в фоновом режиме.
Вернувшись в URLconf вашего проекта, создайте новый путь в smoke_some_meats:
# hello_async/urls.py from django.contrib import admin from django.urls import path from hello_async.views import index, async_view, sync_view, smoke_some_meats urlpatterns = [ path("admin/", admin.site.urls), path("smoke_some_meats/", smoke_some_meats), path("async/", async_view), path("sync/", sync_view), path("", index), ]
Вернувшись в свои представления, создайте новую асинхронную функцию под названием smoke. Эта функция принимает два параметра: список строк с именем smokables и строку с именем flavor. По умолчанию это список пустой и flavor имеет значение «Sweet Baby Ray’s» соответственно.
# hello_async/views.py async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> None: """ Smokes some meats and applies the Sweet Baby Ray's """ if smokables is None: smokables = [ "ribs", "brisket", "lemon chicken", "salmon", "bison sirloin", "sausage", ] if (loved_smokable := smokables[0]) == "ribs": loved_smokable = "meats" for smokable in smokables: print(f"Smoking some {smokable}....") await asyncio.sleep(1) print(f"Applying the {flavor}....") await asyncio.sleep(1) print(f"{smokable.capitalize()} smoked.") print(f"Who doesn't love smoked {loved_smokable}?")
Первая строка функции инстанцирует список мяса по умолчанию, если список не задан. Второй оператор «if» затем устанавливает переменную с именем loved_smokable для первого объекта в smokables, если первый объект не является «ribs». Цикл for асинхронно применяет flavor (читай: Sweet Baby Ray) к smokables (читай: smoked meats).
Не забудьте про импорт:
from typing import List
List используется для дополнительных возможностей набора текста. Это не обязательно, и его можно легко пропустить (просто исключите: List [str] после объявления параметра «smokables»).
Затем создайте асинхронное представление, которое использует функцию async smoke:
# hello_async/views.py async def smoke_some_meats(request) -> HttpResponse: loop = asyncio.get_event_loop() smoke_args = [] if to_smoke := request.GET.get("to_smoke"): # Grab smokables to_smoke = to_smoke.split(",") smoke_args += [[smokable.lower().strip() for smokable in to_smoke]] # Do some string prettification if (smoke_list_len := len(to_smoke)) == 2: to_smoke = " and ".join(to_smoke) elif smoke_list_len > 2: to_smoke[-1] = f"and {to_smoke[-1]}" to_smoke = ", ".join(to_smoke) else: to_smoke = "meats" if flavor := request.GET.get("flavor"): smoke_args.append(flavor) loop.create_task(smoke(*smoke_args)) return HttpResponse(f"Smoking some {to_smoke}....")
Это представление принимает необязательные параметры запроса to_smoke и flavor. to_smoke — это список мяса, которое нужно закоптить, через запятую, а flavor — это вкус, кторый вы к ним применяете.
Первое, что делает это представление (чего нельзя сделать в стандартном представлении синхронизации) — захватывает цикл событий с помощью asyncio.get_event_loop(). Затем он анализирует параметры запроса, если применимо (и выполняет некоторую очистку строк для окончательного оператора print). Если мы не передаем ничего для копчения, to_smoke по умолчанию принимает значение «meats». Наконец, возвращается ответ, чтобы сообщить пользователю, что он готовит вкусное барбекю.
Отлично. Сохраните файл, затем вернитесь в свой браузер и перейдите по адресу http://localhost:8000/smoke_some_meats/. Вас должен встретить ответ:
Smoking some meats....
В консоли вы должны увидеть:
Smoking some ribs.... INFO: 127.0.0.1:56239 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK Applying the Sweet Baby Ray's.... Ribs smoked. Smoking some brisket.... Applying the Sweet Baby Ray's.... Brisket smoked. Smoking some lemon chicken.... Applying the Sweet Baby Ray's.... Lemon chicken smoked. Smoking some salmon.... Applying the Sweet Baby Ray's.... Salmon smoked. Smoking some bison sirloin.... Applying the Sweet Baby Ray's.... Bison sirloin smoked. Smoking some sausage.... Applying the Sweet Baby Ray's.... Sausage smoked. Who doesn't love smoked meats?
Обратите внимание, как ребра (ribs) начали коптиться до того, как был зарегистрирован ответ 200. Это асинхронность в действии: поскольку функция smoke изначально спит на одну секунду, представление завершает обработку и возвращает ответ. Конечный пользователь увидит ответ, как только мясо начнет коптиться.
Также стоит отметить, что если вы используете сервер разработки Django, ваш сервер вернет ответ без ошибок, но ничего асинхронного просто не произойдет. Вот как будет выглядеть журнал вашей консоли:
Smoking some ribs.... [16/Aug/2020 22:37:03] "GET /smoke_some_meats/ HTTP/1.1" 200 22
Используя Uvicorn, мы также можем протестировать, используя параметры запроса. Попробуйте http://localhost:8000/smoke_some_meats?to_smoke=ice cream, bananas, cheese&flavor=Gold Bond Medicated Powder. (пробелы будут преобразованы автоматически)
Браузер:
Smoking some ice cream, bananas, and cheese....
Терминал:
Smoking some ice cream.... INFO: 127.0.0.1:56407 - "GET /smoke_some_meats/?to_smoke=ice%20cream,%20bananas,%20cheese&flavor=Gold%20Bond%20Medicated%20Powder HTTP/1.1" 200 OK Applying the Gold Bond Medicated Powder.... Ice cream smoked. Smoking some bananas.... Applying the Gold Bond Medicated Powder.... Bananas smoked. Smoking some cheese.... Applying the Gold Bond Medicated Powder.... Cheese smoked. Who doesn't love smoked ice cream?
Q: Что, если вы сделаете синхронный вызов внутри асинхронного представления?
То же самое, что произошло бы, если бы вы вызывали неасинхронную функцию из неасинхронного представления.
—
Чтобы проиллюстрировать это, создайте новую вспомогательную функцию в файле views.py с именем oversmoke:
# hello_async/views.py def oversmoke() -> None: """ If it's not dry, it must be uncooked """ sleep(5) print("Who doesn't love burnt meats?")
Очень просто: мы просто синхронно ждем пять секунд.
Создайте представление, которое вызывает эту функцию:
# hello_async/views.py async def burn_some_meats(request): oversmoke() return HttpResponse(f"Burned some meats.")
Наконец, подключите маршрут в URLconf вашего проекта:
# hello_async/urls.py from django.contrib import admin from django.urls import path from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats urlpatterns = [ path("admin/", admin.site.urls), path("smoke_some_meats/", smoke_some_meats), path("burn_some_meats/", burn_some_meats), path("async/", async_view), path("sync/", sync_view), path("", index), ]
Посетите сайт в браузере по адресу http://localhost:8000/burn_some_meats:
Burned some meats.
Обратите внимание, как потребовалось пять секунд, чтобы наконец получить ответ от браузера. Вы также должны были одновременно получить вывод консоли:
Who doesn't love burnt meats? INFO: 127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK
Возможно, стоит отметить, что одно и то же произойдет независимо от того, какой сервер вы используете, будь то WSGI или ASGI.
Q: Что, если вы сделаете синхронный и асинхронный вызовы внутри асинхронного представления?
Синхронные и асинхронные представления лучше всего подходят для соответствующих целей. Если у вас есть функция блокировки в асинхронном представлении, в лучшем случае это будет не лучше, чем просто использование синхронного представления. Поэтому в этом нет смысла и этого лучше не делать.
Если вам нужно сделать синхронный вызов внутри асинхронного представления (например, для взаимодействия с базой данных через ORM Django), используйте sync_to_async как оболочку или декоратор.
Пример:
# hello_async/views.py async def async_with_sync_view(request): loop = asyncio.get_event_loop() async_function = sync_to_async(http_call_sync) loop.create_task(async_function()) return HttpResponse("Non-blocking HTTP request (via sync_to_async)")
Не забудьте добавить импорт:
from asgiref.sync import sync_to_async
Добавьте URL:
# hello_async/urls.py from django.contrib import admin from django.urls import path from hello_async.views import ( index, async_view, sync_view, smoke_some_meats, burn_some_meats, async_with_sync_view ) urlpatterns = [ path("admin/", admin.site.urls), path("smoke_some_meats/", smoke_some_meats), path("burn_some_meats/", burn_some_meats), path("sync_to_async/", async_with_sync_view), path("async/", async_view), path("sync/", sync_view), path("", index), ]
Проверьте это в своем браузере по адресу http://localhost:8003/sync_to_async/
В вашем терминале вы должны увидеть:
INFO: 127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK 1 2 3 4 5 <Response [200 OK]>
При использовании sync_to_async блокирующий синхронный вызов обрабатывался в фоновом потоке, что позволяло отправлять HTTP-ответ перед первым вызовом sleep.
Q: Необходим ли Celery для асинхронных представлений Django?
Это зависит….
Асинхронные представления Django предлагают те же функции, что и задачи или очередь сообщений. Если вы используете (или рассматриваете) Django и хотите сделать что-то простое (например, отправить электронное письмо новому подписчику или вызвать внешний API), асинхронные представления — отличный способ сделать это быстро и легко. Если вам нужно выполнять гораздо более тяжелые и длительные фоновые процессы, вы все равно захотите использовать Celery или RQ.
Следует отметить, что для эффективного использования асинхронных представлений в представлении должны быть только асинхронные вызовы. Очереди задач, с другой стороны, используют рабочие процессы в отдельных процессах и, следовательно, могут выполнять синхронные вызовы в фоновом режиме на нескольких серверах.
Кстати, вы ни в коем случае не должны выбирать между асинхронными представлениями и очередью сообщений — вы можете легко использовать их в тандеме. Например: вы можете использовать асинхронное представление, чтобы отправить электронное письмо или внести одноразовую модификацию базы данных, но попросите Celery очищать вашу базу данных в запланированное время каждую ночь или создавать и отправлять отчеты о клиентах.
Для проектов с нуля используйте асинхронные представления и напишите свои процессы ввода-вывода максимально асинхронным образом. Тем не менее, если большинству ваших представлений просто нужно совершать вызовы в базу данных и выполнять некоторую базовую обработку перед возвратом данных, вы не увидите значительного увеличения скорости (если таковое имеется) по сравнению с простым использованием синхронных представлений.
Если у вас мало или совсем нет процессов ввода-вывода придерживайтесь синхронизирующих представлений. Если у вас есть несколько процессов ввода-вывода, оцените, насколько легко будет переписать их асинхронным способом. Переписать синхронизирующий ввод-вывод в асинхронный режим непросто, поэтому вы, вероятно, захотите оптимизировать синхронизирующий ввод-вывод и представления, прежде чем пытаться переписать его в асинхронный режим. Кроме того, никогда не стоит смешивать синхронные процессы с вашими асинхронными представлениями.
В производственной среде обязательно используйте Gunicorn для управления Uvicorn, чтобы воспользоваться преимуществами параллелизма (через Uvicorn) и параллелизма (через рабочие процессы Gunicorn):
gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application
В заключение, хотя это был простой вариант использования, он должен дать вам приблизительное представление о возможностях, которые открывают новые асинхронные представления Django. Некоторые другие вещи, которые можно попробовать в ваших асинхронных представлениях, — это отправка электронных писем, вызов сторонних API и запись в файл. Подумайте о тех представлениях в вашем коде, в которых есть простые процессы, которые не обязательно должны возвращать что-либо непосредственно конечному пользователю — их можно быстро преобразовать в асинхронный.
Чтобы узнать больше о новооткрытой асинхронности Django, прочтите этот отличный пост, посвященный той же теме, а также многопоточности и тестированию.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Перевод статьи "Speeding Up Python With Concurrency, Parallelism and asyncio" - "Разгоняем Python с помощью Конкурентности, Параллелизма и asyncio"
Не, ну вы серьёзно?
В статье про асинхронные запросы рассказываете как создавать новое приложение django?
налито много воды. Пример с этим мясом, "простая" задача в фоновом режиме - вообще никуда не годится и не читается. Автор явно не ищет простых путей