面向对象知识补充

1、getattr(self)

2、__getattr__(self, item)

3、__getattribute__(self, item)

4、@property

drf(纯净版项目)

settings.py基本配置,可参考

 

drf(认证)

对匿名用户添加配置防止报错

如何进行用户的认证

添加全局验证配置

解决状态码一致问题

这个现象需要在源码中解释

状态码一致问题

拓展:子类约束

需要让自己的子类里面必须有某个方法的时候

使用raise NotImplementedError

此时的New里面必须有f1方法,否则就会报错

在drf中的体现:

子类约束

案例:用户登录注册

drf(权限)

自定义错误信息

权限组件的源码流程

权限源码流程

从源码也可以看出,当进行权限认证时

遇到一个报错了后面的就不会再执行了

image-20251214104646292

当有一个返回了False就会执行permission_denied报错拦截

我们可以通过更改这个逻辑来实现不一样的效果:

 

权限组件的拓展(更改逻辑)

image-20251214105504151

在执行的过程中会优先在自己的方法里面寻找check_permissoins

如果自己的方法里面没有的话才会去父类中找

所以可以在自己的类里面定义一个check_permissoins方法

达到重写逻辑的目的

在类里面添加方法修改

改为只要有一个返回为True就通过验证

都返回False才会报错

案例:权限处理

这样就可以实现识别用户的权限,并且根据权限拦截访问

 

补充

django中的中间件执行时机认证组件,权限组件之前

 

drf(限流)

快速上手

源码和具体实现

案例

拓展(自定义限流类抛出的异常信息)

在分析源码执行流程时可以看出请求被限制时会进入wait方法

如果想要自定义异常信息的话只需要重写wait方法即可

关注源码的流程,只有当请求收到限制的时候,也就是超过了限流类允许的最大次数时,wait方法才会被调用,所以当需要自定义抛出的异常信息时,只需要在wait方法里面抛出自定义的错误信息就可以了

 

drf(版本)

参数传递

方式一(基于GET参数)

此时会自动的读取query请求头中携带的版本信息

比如此时?name=vhsj&age=20&version=v2

此时会自动读取请求头中的version信息

并赋值给request.version

image-20251229162658443

从源码中可以看出,获取的请求头中的名字取决于配置文件中的VERSION_PARAM

可以通过配置这个信息来自定义获取的参数名

此时就可以使用v来传递版本参数

最佳实践:

使用默认的version来传递参数名称

####

反向生成url

首先分析源码流程

drf版本(反向生成url)

了解源码流程以后就可以执行以下操作反向生成url

image-20251229172034948

注意此时反向生成的url是只会携带你的版本号参数,如果有其他参数他不会自动携带

方式二(基于路由URL)

方式三(Accept请求头)

image-20260104162634689

这样也可以获取到版本信息,且同样可以反向生成URL

最佳实践

以上三种传递参数的方式

最常用的是方式二

其次是方式一

方式三几乎不使用

补充

为了方便可以这样写

在定义类的时候就不用每次都写一遍版本信息的相关配置了

 

drf(解析器)

解析器:解析请求者发送过来的数据,根据请求头的类型匹配类,然后在类里面对不同的数据进行解析之后赋值给request.data

基本用法和流程

drf(解析器基本流程)

image-20260104170227805

image-20260104170454977

此时可以通过request.data打印获取到的数据

最佳实践:

使用JSONParser

因为JSON对于多层嵌套的格式易于处理

源码流程

drf(解析器源码流程)(创建时)

在执行request.data时才会去触发解析器的动作

如果调用多次

那只会解析一次

drf(解析器源码流程)(执行时)

文件上传

拓展(自定义解析器配置)

许多开发者一般都不写解析器的配置,drf内部有默认值

在学习阶段建议手动编写配置

输出结果

可以通过在settings.py中通过配置来修改这一项

输出结果

 

drf(序列化)

前置知识(元类)

以下两部分的创建类的效果完全一样

都是使用的type创建类,只是表现的形式不一样

type("Foo", (object,), {"v1": 123, "func": lambda self: 999})

会去执行这个类里面的__init__方法

所有的类在执行__init__方法之前会先执行一个__new__方法

__new__方法会先创建一个空值,然后再让__init__方法去这个空值里面去初始化数据

所以创建类本质上是在__new__方法中执行的

演示:

基本使用(快速功能演示)

序列化的本质是把数据库中获取到的数据转换成JSON格式

并返回给用户, 已有的json.dumps()方法由于使用类型受限,只能转换原生的python中的数据类型

所以需要用到drf的序列化

image-20260306160030025

这是用于测试的数据库表

想要使用序列化首先要定义一个类

这个类里面的字段使用serializers.字段类型(定义时的数据类型)

这里面有哪些字段,后面的对象就可以获取到哪些字段

上面的演示代码会输出

基本使用(支持QuerySet)

在上一节点中使用的depart_obj是一个字典类型的对象

在实际开发中我们遇到的不一定是这样简简单单的对象,可能是queryset类型的

通过指定many=True就可以让他支持queryset类型的数据

image-20260306161106291

注意此处的名称类型需要对应

此时输出的内容:

基本使用(Model)

每次使用Serializer都需要把需要的字段都重新写一遍显得有点麻烦

我们可以使用ModelSerializer来简化写法

就像这样:

输出示例:

基本使用(source和时间)

首先创建一个模型

插入两条数据

image-20260307150830463

现在

发送请求后得到的返回的数据是这样的:

gender字段如何显示名称

利用source指定,利用Django中的知识

指定source="get_gender_display"

注意此时获取到的“男”/“女”是CharField,所以需要改成CharField

如果说同字段类型重名,则会报错,比如如果我model中定义的是CharField,我这里再自定义一个CharFIeld,那就会报错,不同类型重名则会覆盖

depart外键怎么显示名称

指定source="depart.title"

本质上:

ctime时间字段怎么格式化

利用format=""

将时间格式格式化

输出:

自定义字段内容

自定义字段名称是xxx的话方法就是get_xxx

方法里面的obj就是当前的字段对象

示例返回:

基本使用(如何处理ManyToManyField类型)

方法一

示例model:

此时由于UserInfo中返回的tags字段是一个queryset类型的数据,无法利用source解决

可以使用自定义方法处理

处理实例:

方法二(最佳实践)

利用类的嵌套

在新创建的类里面对于这种类型的数据添加many=True实现获取到所有对应的数据

返回示例:

注意上述的D1和D2类里面的字段也可以自定义,和普通的类里面自定义的方法一致

小技巧

对于两个类里面都需要使用同一个字段的情况,可以使用继承

示例:

这样在D类里面就自带了xx这个字段,就不用再自己重新写了

源码解析

创建字段对象

drf源码_创建字段对象

创建类

上一步中创建了字段了,按照执行的流程,这一步到了创建类

drf_实例化类

序列化过程

drf序列化过程

数据校验

基本校验

写法一(最佳实践)

写法二

第二种写法关键在于传递了raise_exception=True

这样的话如果校验成功他就会继续往下执行

如果校验失败就会抛出异常,并且让异常信息返回,drf会自动捕获这个异常,不会中断程序

演示:

校验成功:

校验失败:

这种写法的坏处在于抛出的异常信息不方便控制

内置校验和正则校验

内置校验

在创建类的时候添加校验规则

上面的level里面定义的choices中数字的类型程序会自动帮我们处理,实际开发中建议保持规则

比如在上述实例中:

输入

程序打印

会自动处理类型

正则校验

上述中有一种特殊的

这种写法几乎等价于:

邮箱可以用内置的校验规则,也可以自己编写校验规则

钩子校验

对于Serializer

在定义字段的时候,可以在下面定义validate_字段名的钩子方法,会进行相应的校验

value是传进去的旧值,返回的值是校验后的,如果在最后修改了返回的值,他会反映到validated_data里面

上面说的是局部的字段的钩子,还有全局的钩子,全局的钩子会在所有局部的钩子校验通过后执行

在这里面和Django一样,attrs和validated_data名字不同但是所指向的内存地址是相同的,在执行完了所有的校验方法后,attrs会直接引用到validated_data

小拓展(异常字段名)

如果抛出了异常,全局的钩子的异常和局部的钩子的异常的信息不一样

一个是字段名,一个是non_field_errors

后者可以在配置中修改(settings.py)

此时:

一般不用

对于ModelSerializer

这里面同样可以添加钩子方法,主要区别在于校验的参数需要通过字典的形式放进去

数据存储(普通字段)

对于Serializer

可以把数据打散放进去

对于ModelSerializer

可以直接调用save方法

调用save方法直接存储会面临两个小问题

问题一:

如果数据库中有三个字段,但是用户只传了两个字段

就需要通过save方法传参自定义字段

ser.save(count=100)像这样

问题二:

如果数据库中只有三个字段,但是用户传了四个字段

就需要在最后删除字段

ser.validated_data.pop('more')

像这样,在保存的时候就不会提交多余的字段

数据存储(ForeignKey和ManyToManyField)

对于ForeignKey,直接传入对应的关联的id即可,如果传入的id不存在,会报错,存在则自动创建

如果有更多的校验规则,可以通过添加钩子函数来完成,此时的钩子函数里面的value是depart中的一个对象


对于ManyToManyField,传入一个列表,列表里面是需要关联的表中的id即可

像这样传入,会自动关联相关的信息以及映射关系

如果添加了钩子方法,钩子方法里面的value是一个列表,列表里面是符合规则的对象

拓展(ListField字段)

如果对于ManyToManyField存储希望进行自定义,那接受数据类型的时候可以选择serializers.ListField()

示例:

像这样也可以和之前一样实现功能,还可以在里面加入自己想要自定义的内容

唯一的区别是原生的是传递的list类型的,现在传递的是QuerySet类型的,效果一样

校验的同时进行序列化

image-20260309143612404

从而拿到返回的数据,也可以不单独写一个Serializer,共用一个Serializer,只不过这样的话返回的东西就不可以自定制了,显得很局限,但是每次都写两个类就显得很麻烦,此时就可以使用两个参数来控制

read_only看字面意思就是只读的,用这里的场景来解释就是只能在序列化(读取查看)的时候使用,写的时候不能写(此处的原因是因为他是自动生成的)

write_only看字面意思就是只写的,用这里的场景来解释的话就是只能在校验(写入)的时候使用,读的时候不能读

实际使用场景:

给用户返回性别信息时返回性别的信息而不是返回数字

还可以这样实现:


对于外键类型的可以通过嵌套实现数据的显示,但是如果要让名字的对应关系不出问题可以使用source参数

注意

当只写了一个类的时候,就可以不这样写了

直接这样写:

当只写一个的时候内部会自动处理,两个的效果都是一样的

拓展(重写to_representation方法)

在上述校验的同时序列化的时候,会遇到一个问题,当编写一个类的时候,以gender举例,怎么实现一个类的情况下,让gender在写的时候用choices,读的时候为“男”/“女”呢?

首先将Serializer类中to_representation方法源码复制过来准备修改

修改后:

上述的里面是自定义的nb_方法名,实际开发中可以自己自定义

也可以自己自定义一个基类,避免每次都重新编写to_representation方法

案例学习补充

参数的传递

获取参数的方法:(两种,分情况使用)

校验时常用的数据

initial_data未经处理的原始数据(从前端接收到的)【钩子校验里面使用】

validated_data经过了所有验证方法后的数据【钩子钩子校验流程之后使用】

attrs在全局验证函数中的经过了字段级校验的所有参数【全局钩子校验函数中使用】

request.data前端传递过来的body中的数据(请求体参数)

request.query_params.get('')获取url链接中携带的参数(类似?name=jelee)

确认密码的坑

我们在编写确认密码字段的时候,由于数据库中并没有这个字段

所以在保存的时候需要先把validated_data里面的这个字段pop

且需要添加write_only=True

代码示例:

添加write_only的原因是如果不添加的话,虽然在写入的时候已经删除了,但是在后面查看的时候会默认去数据库中读取这个字段,但是这个字段在数据库中是没有的,就会报错

登陆案例中应该用instance还是data

案例场景:

此处如果错误的使用了instance,会导致问题,容易疑惑的点是:

登陆不是比较已有的信息吗,为什么要用data

校验也要用data,因为此时你不知道用户输入的账号密码是否正确,如果不正确,那怎么找到已有信息

save()方法中不受序列化影响

利用这一点可以实现一些功能

比如:如果需要外键用户名输入到数据库里面的时候是id,序列后期输出的时候是用户名

然后在保存的时候

这样指定保存的时候的某些字段就可以避免序列化器的影响

drf(分页)

防止数据过多时一次性请求量过大

PageNumberPagination

基本使用方式

首先需要在配置中配置每页显示几条数据

settings.py

然后处理数据

处理示例:

导入类以后:

利用这个类实例化一个方法,然后调用这个方法的paginate_queryset方法传入参数

待分页的querysetrequest对象self


演示:

传入需要第几页就会返回第几页的数据,所以数据必须是有序的,也就是数据必须经过order_by排序

默认情况不支持传入一页多少条数据(page_size)不过可以自己配置

自定义page_size

配置示例:

注意,如果这里写了page_size配置,全局也写了page_size配置,drf内部会先读取全局的配置,然后再读取局部的配置,在读取局部的配置时会把全局的配置覆盖,所以最终生效的是局部的配置

LimitOffsetPagination

这种分页方式与PageNumberPagination最大的区别是:

PageNumberPagination是告诉他需要第几页,每页几条数据

LimitOffsetPagination是告诉他前面有多少条数据,我需要多少条

基本使用方式

limitoffset的默认值都是0

然后就可以使用了

就会返回两条数据,这两条数据的前面有四条数据,也就是返回第五条和第六条数据

自定义配置

使用:

drf(视图)

GenericAPIView

原始代码

使用示例:

使用GenericAPIView对下面的代码进行简化

参数讲解

利用这些类就可以简化操作

方法讲解

完整流程

最大的意义:

将数据库查询、序列化类提取到类变量中,简化操作

GenericViewSet

查看源码:

image-20260312115042316

可以看出这个类里面类中没有定义任何代码,他就是继承 ViewSetMixinGenericAPIView

那他的功能也就是这两个类融合到一起

示例原始代码

完整代码

别看着改了这么多,实际上就是把get、post这些方法名对应到了具体的方法上

通过「路由映射」把 HTTP 方法(GET/POST/PUT/DELETE)关联到对应的操作(列表 / 创建 / 详情 / 更新 / 删除),能让路由配置更简洁

五大类

GenericViewSet 体系下的 5 个核心 Mixin 扩展类

所以使用的时候都必须继承GenericViewSet

ListModelMixin

原始代码

完整代码

这样更改之后效果一样,只不过返回的数据不一样了,但是可以通过重写list方法达成自定制返回的数据

重写方法(自定义响应格式)

CreateModelMixin

原始代码(手动编写create方法)

完整代码(含重写方法)

拓展(执行流程)

方法会在调用super.create(request, *args, **kwargs)的时候调用perform_create

此处由于有认证组件的需求,所以重写了create方法,如果没有此需求,代码将会非常简洁:

他会自动校验字段(ser.is_valid()

RetrieveModelMixin

原始代码

完整代码

拓展配置

在这种写法中,如果url中的参数名不想写成pk的话就需要添加额外配置

就可以这样写:

UpdateModelMixin

完整代码

drf会自动处理请求,包括了put(全量更新)/patch(局部更新)

put:必须传入所有字段内容更新

patch:传入多少更多少

拓展配置

DestroyModelMixin

完整代码

拓展配置

如果按照上面的进行配置,删除数据后没有任何提示,可以通过重写方法自定制

类的汇总(ModelViewSet)

通过上述代码可以看出如果每次编写类都要继承

那代码显得很冗余,可不可以封装起来呢, drf已经帮我们做了这个事了

ModelViewSet

查看他的源码

image-20260312142926189

就是帮我们汇总了,以后需要就只需要写一个类就可以了

最佳实践

这么多方法怎么取舍呢

如果操作与数据库接口无关,那就直接继承APIView,如果有关,那就继承ModelViewSet,如果功能不够用,就通过重写某些方法进行拓展,根据习惯选择五大类或者ModelViewSet

拓展(自己编写处理方法)

编写url匹配到change_title这个方法

编写change_title方法对应post请求

detail=True的意思是标记为详情级动作,详细参照这个表格

参数值detail=Truedetail=False
URL 是否需 pk是(/demo/1/single-action/)否(/demo/batch-action/)
操作数据范围单条数据(pk 对应的记录)批量数据(所有记录)
能否用 get_object ()可以(获取单条数据)不可以(无 pk,会报错)
函数参数需接收 pk(pk=None)无需接收 pk
典型场景修改单条数据、删除单条数据、点赞单篇博客查询数据列表、批量导出、批量创建

有时候还会需要接受url里面传递过来的参数

这样写的话

image-20260314132305231

就可以达成这样的效果

补充:权限

在之前定义权限类时,类中可以定义两个方法:has_permissionhas_object_permission

所以,让我们在编写视图类时,如果是直接获取间接继承了 GenericAPIView,同时内部调用 get_object方法,这样在权限中通过 has_object_permission 就可以进行权限的处理。

drf(路由)

原始代码

视图类

我们原来定义的url的写法如果里面的参数过多,比如get/post/put这些写的过多了就会显得比较麻烦

基础写法

这样写了以后就相当于完成了和api/blog有关的所有方法的对应

HTTP 方法URL 路径视图方法功能
GET/api/blog/list博客列表
POST/api/blog/create创建博客
GET/api/blog/<int:pk>/retrieve博客详情
PUT/api/blog/<int:pk>/update全量更新
PATCH/api/blog/<int:pk>/partial_update部分更新
DELETE/api/blog/<int:pk>/destroy删除博客

节省了很多时间

额外配置

参数说明:

register的第三个参数basename='front_blog'

这个的意思是

给路由器自动生成的 URL 名称加「基础前缀」,最终形成 namespace:basename-action 的完整 URL 名称(比如 api:blog-list),用于反向解析

如果对应的模型里面有queryset,例如此处制定了views.BlogView,如果BlogView里面制定了queryset,那basename就算不写也会默认指定为那个queryset的模型示例名称

path() 的第一个参数:'api/'

这个的意思是前缀,就是所有通过后面的include挂载的url都会挂载到这个前缀下

include() 的第一个参数:(router.urls, 'api')

这是一个元组类型的对象,第一个时需要包含的url列表对象,也就是上面对router注册的url,用专业术语说就是要引入的「URL 模式列表」

include() 的第二个参数:namespace="api"

这是一个命名空间对象,用于反向解析url的时候不出问题,不会改变url的路径

这种情况下就可以发挥作用:

basename和namespace都只在反向解析url的时候才有用:

反向解析:

多URL写法

写法一(推荐):

写法二:

drf(条件搜索)

自定义搜索规则

第三方Filter

在drf开发中有一个常用的第三方过滤器:

DjangoFilterBackend

安装配置

基本使用

就可以实现id、category的筛选,但是此处只能筛选等于这个条件,不能用其他的

拓展配置

此时发送

就会返回id大于等于21,并且category等于2的数据

当大于小于这些简单的逻辑不能满足我们的需求时,可以自定义一个过滤方法

参数说明:

self: 指向当前的 FilterSet 实例。

queryset: 尚未执行此过滤前的原始查询集(QuerySet)。

name: 字段的名称(在上面的例子中就是 "custom_search")。

value: 前端从 URL 传过来的具体数值(比如 ?custom_search=pythonvalue 就是 "python")。

distinct=False:是否去重,默认值为False

比如一篇博文有两个标签pythongo

现在需要搜索有python标签和有go标签的,这篇博文就会匹配两次,返回这样的数据

此时如果添加这个参数就可以避免这个问题

required=False:是否必填,默认值为False

lookup_expr参数

表达式说明数据库 SQL 逻辑 (示例)常用业务场景
exact精确匹配WHERE col = 'val'匹配 ID、分类 ID、状态码
iexact精确匹配 (忽略大小写)WHERE col ILIKE 'val'匹配用户名、验证码、颜色名
contains包含 (区分大小写)WHERE col LIKE '%val%'搜索文章正文中的特定词汇
icontains包含 (忽略大小写)WHERE col ILIKE '%val%'最常用:搜索标题、简介关键词
startswith以...开头WHERE col LIKE 'val%'搜索姓氏、特定号段的手机号
istartswith以...开头 (忽略大小写)WHERE col ILIKE 'val%'搜索 URL 前缀、文件路径
endswith以...结尾WHERE col LIKE '%val'搜索特定后缀的邮箱
iendswith以...结尾 (忽略大小写)WHERE col ILIKE '%val'搜索图片格式 (如 .JPG)
gt大于 (>)WHERE col > 10筛选价格高于 100 元的商品
gte大于等于 (>=)WHERE col >= 10筛选 18 岁及以上的用户
lt小于 (<)WHERE col < 10筛选库存低于 5 件的预警
lte小于等于 (<=)WHERE col <= 10筛选今天之前创建的文章
in在集合中WHERE col IN (1, 2, 3)批量查询多个 ID 或特定分类列表
range在范围内WHERE col BETWEEN 1 AND 10价格区间、日期段筛选
isnull是否为空WHERE col IS NULL筛选“未绑定手机号”或“未删除”的数据
regex正则匹配WHERE col ~ 'pattern'复杂的文本规则校验(区分大小写)
iregex正则匹配 (忽略大小写)WHERE col ~* 'pattern'复杂的文本规则校验(忽略大小写)

全局配置

这样以后

但是如果你有多个配置项

内置Filter

OrderingFilter(支持排序)

此时他支持用id和category排序

当你传入

的时候就会以id从小到大排序

SearchFilter(模糊匹配)

此时就会支持以title模糊查询,可以写多个(你想在哪些字段里搜?)

例如我这里:

在这里我配置了title所以就模糊匹配到了title符合的这一项


search_fields里面的参数支持这些规则

符号模式说明 (SQL 逻辑)例子
(无)icontains模糊包含 (默认)search_fields = ['title']
^istartswith以...开头search_fields = ['^title']
=iexact完全匹配search_fields = ['=title']
$iregex正则匹配search_fields = ['$title']

如果用户在搜索框输入了空格,比如 ?search=Python Django

拓展(双下划线跨表)

特性SearchFilter (搜索)OrderingFilter (排序)
代码示例search_fields = ['creator__name']ordering_fields = ['creator__age']
业务场景根据“作者名”或“分类名”搜文章按“作者活跃度”或“部门等级”排文章
深度支持支持多级,如 creator__depart__title支持多级,如 creator__depart__level

当你使用双下划线跨表排序或搜索时,Django 必须执行 JOIN 操作。如果你的 queryset 只是简单的 Blog.objects.all(),Django 在序列化每一条数据时,可能还会再去数据库查一次作者信息,导致性能差

解决方案:

配合 select_related 使用

只要涉及跨表(外键),就在 queryset 中提前“预加载”: