Django REST framework知识点总结(django摩托车)

网友投稿 312 2022-08-26


Django REST framework知识点总结(django摩托车)

本文中会介绍Django REST framework(后续统称DRF)中一些常用功能的使用方法以及框架中的基础概念。希望这些内容能够帮忙大家更好的运用该框架去实现API服务。本文适合那些已经对Django框架、DRF以及RESTful API设计风格有了解的相关人群。关于Django框架,推荐直接去阅读官方文档,目前官方已经推出中文版的文档;对于DRF,笔者依旧还是推荐优先看官方文档(虽然没有中文版);对于RESTful API设计风格,可以查看笔者编写的这篇文章。笔者也会将自己的理解在文中进行阐述,这也算是在和大家交流心得的一个过程。若文中有错误的理解和概念,请大家及时纠正;吸纳大家的建议,对于我来说也是很重要的学习过程之一。

1. 序列化

正常情况下,客户端请求中的数据需要经过转换为model对象,并调用model对象的相关方法实现数据落地。反之,从数据库中获取到的数据(model)需要经过将model对象转换为python的基本数据结构后,交给view层逻辑进行包装并返回给客户端。上述情况基本上会发生在每一个业务请求逻辑中,而这些操作逻辑基本上都相似。针对这种情况,应该思考是否可以把上述逻辑抽象出来作为通用逻辑;这样即可以减少重复编码,同时还可以规范数据转换的操作细节。在DRF中,就是使用序列化器来实现这种理念的。DRF中的序列化器同时肩负着序列化与反序列化的责任。还有一点需要注意:本章节所谈的序列化器只负责model与python的基本数据结构之间的转化,而非客户端请求中等的数据于python的基本数据结构之间的转化。

1.1 实现序列化器的思路

本章节中会介绍一些笔者在实现DRF的序列化器时所使用到的技巧和思路。对于实现序列化器的基本流程,笔者推荐去阅读官方文档进行学习。

1.1.1 只针对需要反序列化的字段进行操作

在针对某一个model编写序列化器之前,可以先对其中的字段进行分析。因为有些字段可能不需要返回给客户端,只是用于API服务内部逻辑处理使用或后台逻辑使用。要注意哪些仅仅是内部使用的字段,这些字段是没有必要反序列化的(对外展示)。同时,也会有一些字段是只有在反序列化的时候是需要的。这是可以给该字段添加上如下属性:

records = serializers.ListField(write_only=True)

1.1.2 控制序列化的字段数量

序列化与反序列化时可能会使用到不同的字段,因此可以使用read_only与write_only属性来进行控制。例如:

records = serializers.ListField(write_only=True) id = serializers.IntegerField(read_only=True)

1.1.3 为可选字段设定default值

如果想做到每次只更新部分字段的功能,那么可以为相应的字段加上default值(一般为None)。这样在后续的逻辑中就可以根据该字段是否为默认值来判断前端是否传入了该字段。

1.1.4 隐藏数据库字段名

如果不想把数据库字段名称对外暴露的话,可以使用source参数为指定model字段起一个用于外部使用的别名。

name = serializers.CharField(source="outside_name", read_only=True)

1.1.5 对关联字段的序列化定义

使用SerializerMethodFiled()定义字段,同时编写一个配套的get_<序列化字段名>的方法。即自定义字段的序列化逻辑,一般用于定义o2m和m2m类型关联字段的序列化逻辑。

class ChargeReadOnlySerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) name = serializers.SerializerMethodField(read_only=True) third_party_service = serializers.SerializerMethodField(read_only=True) def get_name(self, obj) -> int: return Charge.ChargeType(obj.c_type).label def get_third_party_service(self, obj): result = { 'id': obj.third_service.id, 'name': obj.third_service.tps_name, } return result

Tips: 对于关联字段,一般可以采取建立两个对应序列化字段方案:一个字段用于读取反序列化时的数据(write_only=True),一个字段用于处理序列化时的数据(read_only=True)。 处理反序列化时的字段可以采用录入类似于主键或者uuid的方式,即该字段的类型为IntegerField、CharField或者是ListField类型,之后在create和update方法中再使用这些id去获取真正的model对象用于save等操作。 处理序列化时的字段可以使用SerializerMethodField类型的字段定义,通过在对应的get_<序列化字段名>的方法中编写将对应的model对象转换成dict类型来表示即可。

1.1.6 数据校验

反序列化时需要进行数据校验,方法is_vaild()只有反序列化的时候才需要调用。可以针对每一个字段编写对应的校验方法,也可以对所有字段编写统一的校验方法(该方法中还可以加入自定义逻辑来满足特殊需求)。

1.1.7 对于何时使用serializers.ModelSerializer

可以在如下情况中考虑使用:

当一个序列化器只用做反序列化时,则可以考虑通过继承serializers.ModelSerializer来快速实现该序列化器。 当一个model没有多对多关系时,可以考虑使用模型序列化器自动生成。

注意:ModelSerializer序列化外键关系的字段时候是使用primary keys作为关系描述内容,即序列化后外键关系使用主键描述的

1.1.8 Model逻辑封装

可以将model相关的逻辑在序列化器中(validate,create或者update)实现。因为序列化器本身就和model关系密切,因此将model相关的封装写到其内部也合理。同时,这样也能大大精简view的编写。

1.1.9 关于Update方法的实现

为了尽可能的减少对数据库的操作,可以采取如下措施:

在update方法中逐个检查参数是否合法,若不合法则直接返回参数不合法异常。 在使用序列化器时可加入partial=True,允许部分字段更新(但这可能也需要前端的相关逻辑支持)。

核心理念即为尽量在数据入库之前进行参数合规检查,尽量不要让数据库去检查字段值是否合法;这样可以尽可能的降低数据库压力。

1.1.10 对于反序列化时使用到的字段的定义

建议这一类的字段都加上required=False, default=None这两个参数,以便于前端更灵活的传参和后续逻辑更简单的编写。

1.1.11 自定义数据不合法的返回

重写validate()方法即可。如果没有将所有的字段都自定义校验,则最好是调用父类的validate()方法:

return super().validate(attrs)

1.1.12 校验必填字段的方式

利用serializers field的required参数使用这种方式时,如果create和update操作涉及到的必填字段不同,那么就要为两种逻辑建立两个write_only=true的字段。即每种操作使用不同的字段去处理逻辑。 利用validate()方法与serializers field的default参数
。这种方法即是在validate()方法中检查必填参数的值是否为default的值;这种方法比较适用于create和update操作涉及到的必填字段相同。 (推荐)将检验的逻辑放入create()方法与update方法中。这种方法即是将校验必填字段的逻辑移入到create()方法与update方法中,在方法中检查validated_data中是否包含有相应必填参数的key。这么做的前提是必填字段的不能带有default参数以及required=True。

1.1.13 关于数据校验

1.2 序列化使用流程

序列化主要是将把model对象转换成python的基本数据结构。

使用方法:

先获取model类实例对象若为新建对象:直接调用model的create()方法。若为查询或者修改操作:可通过url中的主键或其它查询数据来获取。 在自定义view类中新建序列化类实例使用对应的model类对象实例来创建。如果是批量序列化,则需要添加many=True参数;同时传入多个对象(query_set)即可。 序列化后的数据序列化类实例的data属性为一个dict,其中便是该model类实例对应的内容。 返回体数据序列化如果使用DRF自带的Response对象,则不需要自行对data进行序列化。(因为DRF封装的Response会根据客户端类型自动按需序列化data内容)。若需要自定义返回体数据的序列化方式,则需要自行实现Response对象并将自定义序列化逻辑封装在其内部。

注意:当序列化时指定了many=True,序列化器实例的data属性返回的是一个list,而非dict。

1.3 反序列化使用流程

反序列化主要是将python的基本数据结构转成model对象。

使用方法:

先获取model类实例对象若为新建对象:直接调用model的create()方法。若为查询或者修改操作:可通过url中的主键或其它查询数据来获取。 在自定义view类中新建序列化类实例若为新建对象:不需要model类实例,需要修改的数据若为修改对象:需要model类实例与修改的数据。 调用校验方法is_valid 调用序列化类实例的save方法

2. ViewSet

2.1 ViewSet的优势

如果不使用ViewSet,而是使用APIView(或使用Django CBV)来实现view层,那么只能实现Http method的同名方法(get,put等等)。如果使用ViewSet来编写view,则可以将业务逻辑方法通过action绑定到对应的Http method同名方法。利用该功能,就可以把同类型的业务逻辑都封装在同一个ViewSet类中,即ViewSet与业务逻辑可建立对应关系,刚便于代码的整理与阅读。

Tips: ViewSet的绑定功能实际上都是在ViewSetMixin类中实现的,相当于是给APIView拓展了绑定功能。

2.2 自定义ViewSet

如果不想使用GenericAPIView使用的mixin类,但又想自定义Http method方法名时,可以`让自定义view继承ViewSetMixin和APIView即可。

Tips:实际上就是直接继承rest_framework.viewsets.ViewSet。

2.3 关于ModelViewSet

由于该视图类继承的mixin中的各个操作方法,都是写好的固定逻辑;因此肯定会有不符合自定义逻辑的地方。此时,可以尝试仿写rest_framework.mixins中的各个方法;其次再让自定义view继承自定义的mixin类与GenericViewSet类;这样就可以按照类似于ModelViewSet的方式去写view了。

2.4 关于Mixin

在Django以及DRF内部,有很多的CBV都使用了Mixin来实现的。包括笔者在使用这两者去实现项目需求时,也会仿照它们去实现一些Mixin类。通过实践,笔者也体会到了Mixin的一些特点与优势,在这里给大家分享一下。

Mixin是利用了Python的多重继承机制来实现的。

Mixin的目的就是给一个类增加多个功能。这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixin的功能,而不是设计多层次的复杂的继承关系。可以将常用的逻辑进行通用化的抽象,并将这些逻辑封装到Mixin中。当view需要使用到这些功能时,就可以直接继承相应的Mixin类并直接调用相应的方法即可。

Tips: 笔者个人感觉Mixin的使用方式可能会和面向对象设计当中的一些概念会有冲突。在面向对象设计中是提倡多使用组合或聚合的方式来使用多个类;而Mixin实际上是利用了类之间的继承关系来实现。笔者对于这个事情的看法是:封装到Mixin内的逻辑更多还是和主逻辑关系比较紧密的,因此使用类的继承关系来实现是可以的。因为本身这两个类之间就是一个强耦合的关系。

使用Mixin模式有几点是需要注意: 它必须表示某一种功能,而不是某个物品。 它必须责任单一。如果有多个功能,那就写多个Mixin类 它不依赖于子类的实现。 子类即便没有继承这个Mixin类,也照样可以工作。就是缺少了某个功能。

笔者对上述几点的理解是:Mixin里实现的不是一个子类,而是指封装了一些功能方法的工具类。继承Mixin实际上是为了给子类添加更多的功能,而不是传统的继承父类。即Mixin类是无需实例化的,Mixin类无法单独作为一个类进行对象实例化使用。

对于Mixin的使用细节,这里笔者谈及一点:在继承mixin类时,务必要把所有的mixin类写在继承队列的最前端。这主要是由于Python的多重继承是通过C3算法实现的。如果所有类中没有同名的方法,那可以不用按照上述的要求进行编写。但

3. 自定义认证逻辑

3.1 实现思路

自定义认证类需要继承rest_framework.authentication.BaseAuthentication。 需要重写authenticate方法以及authenticate_header方法(可选)。authenticate方法中主要是写自定义认证的逻辑。例如从request请求中获取token,从而判断token的合法性等等。

3.2 配置

全局开启:在setting中配置相关参数。

REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ['zonebank.auth.ZoneBankAuthentication'] }

局部开启:在继承了APIView的视图中使用authentication_classes类属性来配置(实际上为APIView的类属性)。类属性类型为list,内容为自定义的认证类名。局部关闭:在继承了APIView的视图中使用authentication_classes=[]来关闭全局开启认证。

3.3 使用方式

通过drf的request来使用因为自定义认证类中的authenticate方法返回的tuple中的内容会相应的保存到DRF Http request的user和auth中属性中。因此可以直接在后续的代码中通过使用request.user或request.auth来使用自定义认证返回的数据。

4. 自定义权限

配置的方式类似于认证配置;APIView相关类属性:permission_classes。

5. 自定义解析器

解析器是用来将前端传送过来的数据转化为python的基本数据结构。

注意:与章节1所介绍的序列器要区分开。虽然两者做的事情都是序列化,但操作的目标不同。

DRF自带了Json,File和Html from表单的parser。如果传给api的数据格式比较特殊,则可以建立自定义的解析器来解析。

6.自定义异常

通过重写DRF中views.exception_handler方法来实现,该方法最后应返回一个Response。若想直接复用DRF已有的异常处理机制,则可以在自定义的exception_handler方法中调用DRF的exception_handler,之后在新增的自定义的异常处理逻辑。若不想使用DRF的默认异常响应,则需要自行对DRF的自带异常进行捕获、分析以及构造自定义Response;同时,以同样的方式对自定义的异常进行操作。针对于上述情况,笔者这里给出一个实践思路,供大家参考:

首先为特定的异常编写异常处理器 class AuthErrorHandler(BaseAPIExceptionHandler): def handle(self): response = Response( { 'ErrorCode': 'LOGIN-AUTH-401', 'ErrorMessage': str(self.exc) }, status=status.HTTP_401_UNAUTHORIZED ) return response 将第一步中实现的异常处理器注册到异常处理注册表中 # 需特殊处理的异常 # 以dict的形式注册到该list中 # dict中,key:exception的value为异常类,key:exception的value为相应的异常处理器 error_type = [ { 'exception': BaseAPIException, 'handler': ZoneBankErrorHandler }, { 'exception': AuthenticationFailed, 'handler': AuthErrorHandler }, { 'exception': ValidationError, 'handler': SerializerErrorHandler }, { 'exception': ParseError, 'handler': RequestParseErrorHandler }, { 'exception': MethodNotAllowed, 'handler': MethodNotAllowedErrorHandler }, ] 编写自定义exception_handler方法 def api_exception_handler(exc, context): response = None # 处理特殊异常 for error in error_type: if isinstance(exc, error['exception']): custom_handler = error['handler'](exc, context) response = custom_handler.handle() # 处理未知错误 if not response: default_handler = DefaultErrorHandler(exc, context) response = default_handler.handle() return response

7. 自定义Http Response

通过继承rest_framework.response.Response,并重写其构造方法即可。注意还需要调用父类的构造方法。

Tips: 实际上主要是对构造方法中的data参数进行逻辑重写。将构造方法的入参修改为符合自己逻辑的参数,之后再将新增的参数都作为原data参数的值,其它参数都对应传给原构造方法即可。

例如:

from rest_framework.response import Response class APIResponse(Response): """Http response for API """ def __init__( self, message: str = None, data:dict = None, int = 200, headers:dict = None ): """init """ result = {} # set response message if message: result['Message'] = message else: result['Message'] = 'Request Successfully' # set response data if data: result['Data'] = data # 调用父类构造方法 super().__init__(data=result, status=headers=headers)

8. 实现JWT用户身份认证功能

需要提前安装djangorestframework_jwt第三方依赖包。

8.1 自定义JWT认证类

JWT认证类主要是用来解析客户端中带有的JWT,即做的是检查请求的用户身份合法性。

这里提供两种实现思路:

如果想自定义认证异常返回消息以及获取JWT的方式时,则直接继承BaseJSONWebTokenAuthentication然后重写authenticate方法即可。注意authenticate方法需要返回一个长度为2的Tuple类型变量,并且第一个元素必须是django user类的对象实例。 如果只是想修改jwt的获取方式时,则自定义JWT认证模块的方式为继承BaseJSONWebTokenAuthentication然后重写get_jwt_value方法即可。该思路其实就是在模仿JWT模块自带的那个JSONWebTokenAuthentication方法来实现。

在实现相关方法时,可以使用rest_framework_jwt.settings.api_settings.JWT_DECODE_HANDLER方法来解析出Token中的payload部分。并且,若使用的是Django的user model,则还可以调用rest_framework_jwt.settings.api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER方法来获取payload中的username值,以用于对获取user model对象。

若想自定义JWT接口的Response,则可以重写rest_framework_jwt.utils.jwt_response_payload_handler方法。重写方式见源码方法注释内容(前提:如果使用的是自动签发功能)。

认证类在实现时可以仿照rest_framework_jwt.authentication.BaseAuthentication来编写,修改其中的日志记录以及错误抛出即可。

大致实现逻辑如下: 获取JWT一般都会将JWT放入request请求header中的Authorization字段中,因此认证逻辑中应先去header中获取JWT。使用rest_framework.authentication.get_authorization_header方法 解析payload可以使用章节前半部分所介绍的rest_framework_jwt.settings.api_settings的相关方法来实现。此处不再重复说明。

这里给出笔者曾经实践的认证类,供大家参考:

import jwt from django.conf import settings from django.contrib.auth import get_user_model from rest_framework import HTTP_HEADER_ENCODING from rest_framework.exceptions import AuthenticationFailed from rest_framework_jwt.settings import api_settings from rest_framework_jwt.authentication import BaseAuthentication logger = settings.LOGGER jwt_decode_handler = api_settings.JWT_DECODE_HANDLER jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER def get_authorization_header(request): """获取request中的jwt;支持从cookie中获取 """ auth = request.META.get('HTTP_AUTHORIZATION', b'') if not auth: auth = request._request.COOKIES.get('jwt', b'') if isinstance(auth, str): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth class JWTAuthentication(BaseAuthentication): """JWT认证类 """ def authenticate(self, request): """JWT认证逻辑 """ jwt_value = get_authorization_header(request) if not jwt_value: logger.info(log_msg_to_json_str( msg='获取JWT失败:request header的Authorization字段未包含token;', data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('登陆验证失败.请提供合法的登陆验证信息') try: payload = jwt_decode_handler(jwt_value) except jwt.ExpiredSignatureError: logger.info(log_msg_to_json_str( msg='JWT验证失败:JWT已过期;JWT:{0}'.format(jwt_value), data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('登陆信息已过期.请尝试重新登陆') except jwt.DecodeError: logger.warning(log_msg_to_json_str( msg='JWT验证失败:解析payload失败;JWT:{0}'.format(jwt_value), data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('登陆验证失败.请提供合法的登陆验证信息') except jwt.InvalidTokenError: logger.warning(log_msg_to_json_str( msg='JWT验证失败:接收到非法token;JWT:{0}'.format(jwt_value), data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('非法登陆.请提供合法的登陆验证信息') user = self.authenticate_credentials(payload, request) return (user, jwt_value) def authenticate_credentials(self, payload, request): """验证jwt payload中用户信息的合法性 """ user_model = get_user_model() username = jwt_get_username_from_payload(payload) if not username: logger.warning(log_msg_to_json_str( msg='JWT验证失败:无法获取payload中的username', data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('非法登陆.请提供合法的登陆验证信息') try: user = user_model.objects.filter(name=username).first() except user_model.DoesNotExist: logger.info(log_msg_to_json_str( msg='JWT验证失败:用户名:{0}不存在'.format(username), data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('登陆信息认证失败.用户名不存在') if not user.is_active: logger.info(log_msg_to_json_str( msg='JWT验证失败:用户:{0}未激活'.format(username), data=request.data, client_ip=request.META.get('REMOTE_ADDR', '') )) raise AuthenticationFailed('登陆信息认证失败.该用户名未激活,请联系客户激活该用户后再尝试登陆') return user

8.2 签发JWT

用户在首次请求时,需要先使用个人的用户名以及密码来请求获取JWT。平台在验证用户的身份后会自动生成相应的JWT返回给用户,这样用户在后续的请求中就可以使用JWT来作为身份信息了。基于上述逻辑,一般需要建立一个专用于下发JWT的API。这里提供一些笔者的实践经验供大家参考。

8.2.1 实现JWT签发(生成)逻辑

由于客户端是通过请求相应的Auth API来获取JWT,因此JWT的签发(生成)逻辑可以在相应的序列化器中来实现。同时,可以通过将签发逻辑封装在序列化器的validate方法中;这样在签发JWT时就与序列化器的参数校验方法结合了起来,方便上层view的调用。

使用rest_framework_jwt.settings.api_settings.JWT_PAYLOAD_HANDLER方法来生成jwt的payload部分。其中,需要将对应的user对象传给jwt_payload_handler方法。其次,使用rest_framework_jwt.settings.api_settings.JWT_ENCODE_HANDLER方法生成完整的JWT。

另外,该序列化器是不需要实现create和update方法的。因为不需要该序列化器去保存或修改model对象。

大致实现思路如下: 序列化类关联user类;在其中的validate方法中实现检查用户校验逻辑以及生成jwt的逻辑。 序列化可通过直接把token加入到validate方法的返回值dict中,或者使用序列化类的context属性返回给view。

8.2.2 实现JWT签发接口

完成含有签发JWT逻辑的序列化器后,还需要使用它来实现签发JWT API所对应的View。

首先,在实现该API View时,务必需要将其类属性authentication_classes至为空List。因为如果没有做该操作,则该View在执行相关逻辑前会先去调用章节8.1中所定义的JWT认证类;但该API又为JWT签发接口,因此就会与其需求产生矛盾并导致无法正常签发JWT签发。

其次,由于之前已经将JWT签发逻辑封装在对应的序列化器的validate方法中,那么在View中实现签发逻辑时就可以直接调用序列化器的参数校验逻辑。即view中不会出现其他不相关的逻辑,与普通的view相同,只是调用了序列化器和相关日志记录等。

Tips: 上述情况能够体现出将一部分业务逻辑封装在序列化器中的优点:能够使得view的代码结构更加清晰。

最后,在成功生成了JWT后,可通过使用自定义Http response或DRF的APIResponse来将JWT下发给用户。

这里给出笔者曾经实践的view,供大家参考:

class LoginAPIView(ViewSet): """ 登陆API视图 """ authentication_classes = [] def login(self, request): jwt_serializer = JWTLoginSerializer(data=request.data) try: jwt_serializer.is_valid(raise_exception=True) # 校验用户以及签发JWT except ValidationError as error: logger.warning(log_msg_to_json_str( msg='用户登陆失败;错误原因:数据校验不通过;exc_type:{0};details:{1}'.format( type(error), error ), data=request.data )) raise LoginError(detail='用户登陆失败;提供的用户数据不合法;Details:{0}'.format(error)) jwt = jwt_serializer.context.get('jwt') user = jwt_serializer.context.get('user') return APIResponse(msg='登陆成功', username=user.name, token=jwt)

8.3 注册自定义认证类

最后还需要将自定义认证类注册到DRF中:

REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ["tsmp.auth.JWTAuthentication"] }

这种注册为全局注册,即所有的view都会使用其作为用户认证逻辑。

9. DRF Http Request

DRF为其自身的功能特性而重新封装了Django的Http Request对象。DRF的Http Request对象是基于Django的Http Request对象而实现的,因此对于Django Http Request对象的一些操作也可以作用于DRF Http Request对象上。下面罗列一些DRF Http Request的使用技巧:

获取Django Http Request对象原生的django request可使用DRF request来获取: django_request = request._request DRF的Request与django原生的request使用方式相同其原因是DRF的request写了getattr方法。该方法中对django原生的request进行了反射调用(getattr())。 DRF Http Request body data POST: 只要是post请求中带有的数据,都可以使用DRF的request.data属性来获取,实际上取的是DRF request类对象的_full_data实例属性,该属性为一个dict。因为DRF的request的data方法中对所有途径请求的data都做了特殊处理,保证所有的数据最后都会存储到DRF request类对象的_full_data实例属性中。 GET: 使用DRF的request的query_params方法即可,因为方法中取的还是django原生request的GET。之所以又封装了一层,是为了实现restfull原则中查询/过滤参数是从url传过来的;django原生的GET表达不出该含义。 文件: 使用drf的request的FILES方法即可。但实际上还是调用了_load_data_and_files方法去取。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Python3中类型注解的学习与实践(python3注释)
下一篇:Python 继承(python培训)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~