随着Django的不断发展,现在已经到了4.1版本。但是网上的资料大多数都是2.X版本的资料,很少涉及到新特性的使用案例,那么django的这些新特性,大家都在项目中使用到了吗?
异步支持
在Python3.4开始提供了对异步的支持,在Django 3.1支持使用异步视图和异步中间件,在Django4.1中支持使用异步ORM查询。接下来带大家从创建项目开始,体验如何在Django项目中使用异步。
初始化项目
创建项目首先,我们使用pycharm创建新的项目,指定python解释器版本为3.6以上版本
运行项目我们依然可以使用Django内置开发测试服务器,启动命令为python manage.py runserver
,虽然项目可以正常启动,但实际上它不会真正异步运行它们,因此我们将ASGI来启动项目。 所谓的ASGI简单来说,就是在原来WSGI的基础上,新增了异步和webscoket的支持。 Web服务器可分为WSGI和ASGI两种模式,在不同的模式下,同步和异步视图有性能差别:
- WSGI+同步视图:传统模式
- ASGI+异步视图:性能最佳的异步模式
- WSGI+同步视图+异步视图:传统模式混杂异步,异步视图的性能不能正常发挥
- ASGI+同步视图+异步视图:异步模式混杂传统视图,异步性能最佳
ASGI组件我们有两种应用服务器可以来启动它,一种是用Uvicorn,Uvicorn是基于uvloop和httptools的ASGI服务器,它理论上是Python中最高性能的框架了。另一种是Daphne,Daphne是Django软件基金会开发的一个基于ASGI (HTTP/WebSocket)的服务器。但实际测试两者性能相差不大,由于Uvicorn更加轻量,便于安装,所以此处以Uvicorn为例。用Uvicorn来启动ASGI组件首先先安装Uvicorn。pip install uvicorn
安装好之后我们用下面的命令来启动我们的项目:uvicorn djangoProject.asgi:application
注意:djangoProject为我们的项目名称。
1 2 3 4 5 6
| (venv) ➜ djangoProject uvicorn djangoProject.asgi:application INFO: Started server process [9371] INFO: Waiting for application startup. INFO: ASGI 'lifespan' protocol appears unsupported. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
访问Django项目
可以看到能正确渲染我们django默认页面
pycharm配置项目启动命令
–reload标志告诉uvicorn监视文件中的更改,如果发现更改,则重新加载。
异步视图
异步视图与异步任务在Django项目中可以混合使用异步和同步视图,中间件和测试。Django将在适当的执行上下文中执行每个操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import asyncio import httpx as httpx from django.http import HttpResponse
async def async_task(): async with httpx.AsyncClient() as client: r = await client.get('https://www.example.org/') print(r.status_code)
def sync_task(): r = httpx.get('https://www.example.org/') print(r.status_code)
async def index(request): return HttpResponse("Hello, Django!")
async def async_view(request): loop = asyncio.get_event_loop() loop.create_task(async_task()) return HttpResponse("Hello, async Django!")
def sync_view(request): sync_task() return HttpResponse("Hello, sync Django!")
|
1 2 3 4 5 6 7 8 9 10 11 12
| from django.contrib import admin from django.urls import path
from demo import views
urlpatterns = [ path('admin/', admin.site.urls), path("async/", views.async_view), path("sync/", views.sync_view), path("", views.index), ]
|
- 访问同步视图**
http://127.0.0.1:8000/sync/
**
1 2 3 4 5
| INFO: 127.0.0.1:61377 - "GET /sync/ HTTP/1.1" 500 Internal Server Error 2022-12-24 22:17:17.257 | INFO | demo.views:sync_view:35 - 开始处理请求 2022-12-24 22:17:22.966 | INFO | demo.views:sync_task:17 - 200 2022-12-24 22:17:22.967 | INFO | demo.views:sync_view:37 - 返回响应内容 INFO: 127.0.0.1:61377 - "GET /sync/ HTTP/1.1" 200 OK
|
访问异步视图**http://127.0.0.1:8000/async/
**
1 2 3 4
| 2022-12-24 22:22:14.642 | INFO | demo.views:async_view:27 - 开始处理请求 2022-12-24 22:22:14.643 | INFO | demo.views:async_view:29 - 返回响应内容 INFO: 127.0.0.1:62430 - "GET /async/ HTTP/1.1" 200 OK 2022-12-24 22:22:18.338 | INFO | demo.views:async_task:11 - 200
|
通过控制台打印日志可以看出,Django在执行异步视图时,会直接返回HTTP响应,异步任务仍在执行直到结束。Django中异步视图中调用和执行异步任务是非阻塞的,执行效率非常高。 在我们实际的项目开发过程中,如果视图逻辑中存在发送验证码,调用第三方API,读写文件等异步操作时,如果这些异步操作内容不需要返回给前端用户时,推荐使用异步视图处理。
同步转异步
如果需要在异步视图内调用同步任务(比如通过Django ORM与数据库进行交互),需要使用sync_to_async将同步任务转为异步任务。
1 2 3 4 5 6 7 8
| from asgiref.sync import sync_to_async
async def async_view(request): logger.info('开始处理请求') async_function = sync_to_async(sync_task) asyncio.create_task(async_function()) logger.info('返回响应内容') return HttpResponse("Hello, async Django!")
|
控制台日志
1 2 3 4
| 2022-12-26 13:42:29.335 | INFO | demo.views:async_view:36 - 开始处理请求 2022-12-26 13:42:29.336 | INFO | demo.views:async_view:39 - 返回响应内容 INFO: 127.0.0.1:62658 - "GET /async/ HTTP/1.1" 200 OK 2022-12-26 13:42:35.897 | INFO | demo.views:sync_task:25 - 200
|
使用sync_to_async,原本阻塞HTTP响应的同步任务将会放在后台线程中处理,从而允许先返回HTTP响应再执行耗时的同步任务。
枚举
在Django3.0推出了全新的枚举类型,相较于之前的枚举写法更加有利于代码封装和阅读。Django 3.0现在提供了一个有两个子类(IntegerChoices和TextChoices)的Choices类。它们扩展了Python的枚举类型,它是一种定义和约束模型Field.choices的更好方法。
定义枚举类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from django.db import models
class Gender(models.IntegerChoices): man = 1, '男' woman = 2, '女' secrecy = 0, '保密'
class UserDemo(models.Model): name = models.CharField('姓名', max_length=25) age = models.IntegerField('年龄') gender = models.IntegerField(verbose_name='性别', choices=Gender.choices, default=Gender.secrecy)
def __str__(self): return self.name
class Meta: verbose_name = '用户测试' verbose_name_plural = '用户测试'
|
使用枚举类型
1
| user = UserDemo.objects.filter(gender=Gender.man)
|
1 2 3 4 5 6 7 8 9 10 11
| from demo.models import UserDemo, Gender user = UserDemo.objects.filter(gender=Gender.man).first()
user.gender
Gender(user.gender).name
Gender(user.gender).label
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Gender(value=1).name
Gender(value=1).label
Gender.choices
Gender.names
Gender.values
Gender.labels
|
时区
在python3.9推出了一个可以实现时区的模块zoneinfo,替代了过去的pytz库,该模块默认情况下是使用系统时区的数据。 Django对zoneinfo时区模块支持如下:
- Django3.2是个过渡版本,可以使用非pytz的时区库。
- Django4.0中,zoneinfo成为默认时区库。
- Django5.0中,pytz会被移除。
从pytz改为使用zoneinfo,基本是透明无感的。比如当前时区的选择、在表单和模板中时区的转换等等操作都不受影响。 在Django中使用时区时,建议设置 USE_TZ = True 后,处理时间方面,有两条 “黄金法则”: 保证存储到数据库中的是 UTC 时间; 在函数之间传递时间参数时,确保时间已经转换成 UTC 时间;
快速体验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import zoneinfo from datetime import datetime, timedelta, timezone
tz = zoneinfo.ZoneInfo("Asia/Shanghai")
date_utc = datetime(2022, 12, 20, 20, tzinfo=timezone.utc) date_tz = date_utc.astimezone(tz) date_tz_dst = date_tz + timedelta(hours=1) print("utc时间:", date_utc) print("tz时间:", date_tz) print("tz_dst时间:", date_tz_dst) print("时区简称:", date_utc.tzname())
|
获取当前时区时间
1 2 3 4
| import zoneinfo from django.utils import timezone tz = zoneinfo.ZoneInfo("Asia/Shanghai") now = timezone.now().astimezone(tz=tz)
|
获取传入时间参数
1 2 3 4 5 6 7 8
| rom datetime import datetime import zoneinfo tz = zoneinfo.ZoneInfo("Asia/Shanghai")
time_start = datetime.strptime(time_after, '%Y-%m-%d %H:%M:%S').astimezone(tz=tz) time_end = datetime.strptime(time_before, '%Y-%m-%d %H:%M:%S').astimezone(tz=tz)
data = Data.objects.filter(time__range=[time_start, time_end])
|
缓存
在Django4.0中新增了新的django.core.cache.backends.redis.RedisCache缓存后端为使用 Redis 进行缓存提供了内置支持。不再需要像以前借助第三方django-redis
库实现Django操作redis
安装配置
1 2
| pip install redis # 必装 pip install hiredis # 可选,可以提高redis批量解析的速度
|
- 修改setting
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': 'redis://:123.com@127.0.0.1:6379/1', 'OPTIONS': { 'parser_class': 'redis.connection.HiredisParser', } } }
|
cache使用
1 2 3 4
|
from django.core.cache import cache cache.set("message","1111",timeout=3600)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| value = cache.get("message") if value: 走缓存 else: 走数据库 并且设置缓存
>>> from django.core.cache import cache >>> cache.set("foo", "value", timeout=25) >>> cache.ttl("foo") 25 >>> cache.ttl("not-existent") 0
|
1 2 3 4 5 6 7
| cache.delete("a")
cache.delete(["a", "b", "c"])
cache.clear()
|
webscoket
从Django3.0开始提供了开箱即用的 ASGI 支持。我们可以不用借助第三方组件(例如channels),直接在ASGI应用程序中处理Websocket连接,发送和接收数据以及实现业务逻辑。
创建ASGI应用程序
在默认的asgi.py文件中,仅能处理http请求,显然是不能满足需求的,因此我们需要对asgi中的application进行改造,让它处理webscoket请求。我们先定义一个名为 application 的异步函数,它接受 3 个 ASGI 参数:scope、receive和send。 然后将 get_asgi_application 调用的结果重命名为 django_application,因为我们需要它处理 HTTP 请求。 在我们的应用程序函数中,我们将检查 scope[‘type’] 的值以确定请求类型。 如果请求类型是 ‘http’,那么这个请求就是一个普通的 HTTP 请求,我们应该让 Django 来处理它。 如果请求类型是“websocket”,那么我们需要自己处理逻辑。 新的 asgi.py 文件应如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import os
from django.core.asgi import get_asgi_application
from djangoProject.websocket import websocket_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoProject.settings')
django_application = get_asgi_application()
async def application(scope, receive, send): if scope['type'] == 'http': await django_application(scope, receive, send) elif scope['type'] == 'websocket': await websocket_application(scope, receive, send) else: raise NotImplementedError("Unknown scope type {scope['type']}")
|
接下来定义一个websocket处理函数,在 websocket_application 函数内部,我们将定义一个无限循环,它将处理 Websocket 请求,直到连接关闭。 在该循环中,我们将等待服务器从客户端接收到的任何新事件。 然后我们将对事件的内容采取行动,并将响应发送给客户端。 当一个新的 Websocket 客户端连接到服务器时,我们会收到一个 ‘websocket.connect’ 事件。 为了允许此连接,我们将发送一个“websocket.accept”事件作为响应。 这将完成 Websocket 握手并与客户端建立持久连接。 当客户端终止与服务器的连接时,我们还需要处理断开连接事件。 为此,我们将监听“websocket.disconnect”事件。 当客户端断开连接时,我们将跳出无限循环。 最后,我们需要处理来自客户端的请求。 为此,我们将监听“websocket.receive”事件。 当我们收到来自客户端的“websocket.receive”事件时,我们获取 event[‘text’] 的值是就是客户端发送的内容。服务端将发送一个“websocket.send”事件,实现与客户端的通信。 设置好 Websocket 逻辑后,我们的 websocket.py 文件应该如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from loguru import logger
async def websocket_application(scope, receive, send): while True: event = await receive() if event['type'] == 'websocket.connect': await send({ 'type': 'websocket.accept' }) logger.info("客户端建立连接") if event['type'] == 'websocket.disconnect': logger.info("客户端断开连接") break
if event['type'] == 'websocket.receive': logger.info("收到客户端信息%s" % event['text']) await send({ 'type': 'websocket.send', 'text': 'hello websocket!' })
|
访问测试
使用websocket工具连接服务端。
服务端控制台打印
1 2 3 4 5 6 7
| INFO: ('127.0.0.1', 54746) - "WebSocket /" [accepted] 2022-12-26 11:29:28.254 | INFO | djangoProject.ws:websocket_application:11 - 客户端建立连接 INFO: connection open 2022-12-26 11:29:35.190 | INFO | djangoProject.ws:websocket_application:17 - 收到客户端信息hello 2022-12-26 11:29:37.520 | INFO | djangoProject.ws:websocket_application:13 - 客户端断开连接 INFO: connection closed
|