django新特性汇总

随着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将在适当的执行上下文中执行每个操作。

  • views.py
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!")
  • urls.py
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
# 1
# 获取枚举值的名称
Gender(user.gender).name
# 'man'
# 获取枚举值的标签
Gender(user.gender).label
# '男'
  • 接收传参值后转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 获取指定value的枚举值名称
Gender(value=1).name
# 'man'
# 或者指定value的枚举值标签
Gender(value=1).label
# '男'
Gender.choices
# [(0, '保密'), (1, '男'), (2, '女')]
Gender.names
# ['secrecy', 'man', 'woman']
Gender.values
# [0, 1, 2]
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())

# utc时间: 2022-12-20 20:00:00+00:00
# tz时间: 2022-12-21 04:00:00+08:00
# tz_dst时间: 2022-12-21 05:00:00+08:00
# 时区简称: UTC

获取当前时区时间

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)
# 使用带时区的时间对象查询数据时,Django内部会自动转为UTC查询数据库
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批量解析的速度
  1. 修改setting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置缓存
CACHES = {
'default': {
# 后端,必选,固定
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
# 必选,格式为:redis://username:password@host:port/db
'LOCATION': 'redis://:123.com@127.0.0.1:6379/1',
# 可选,传递给后端的选项,根据后端的不同,参数也会不同
'OPTIONS': {
# 解析类,可以不指定,如果安装了hiredis就默认使用这个,如果显式指定了这个,就必须安装hiredis
'parser_class': 'redis.connection.HiredisParser',
}
}
}

cache使用

  • 设置cache
1
2
3
4
# timeout=0 立即过期
# timeout=None 永不超时
from django.core.cache import cache
cache.set("message","1111",timeout=3600)
  • 取出cache
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
  • 删除cache
1
2
3
4
5
6
7
#删除某条缓存记录
cache.delete("a")
### 输入参数为该记录的 key
#删除多条缓存记录
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')

# application = get_asgi_application()
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