python之django框架类视图(views)路由反射解读

最近一直在看django框架的rest-framework库,尝试看了一下源码,觉得挺有意思的。这里记录一下自己对于django在使用rest_framework写类视图以及路由分发到类视图函数执行的整个过程,给自己这7秒钟记忆的脑子存个档,方便以后查看。

第一部分 示例代码

依赖包:

	django==2.1.7
	djangorestframework==3.11.0
	mysqlclient==1.4.6
第一步:定义models模型类,这里随便写了一个

在这里插入图片描述

from django.db import models


class School(models.Model):
    avatar = models.ImageField(verbose_name='学校logo', upload_to='images/%Y_%m_%d')
    name = models.CharField(verbose_name='学校名字', max_length=30)

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'ra_schools'
        verbose_name = '学校管理'
        verbose_name_plural = verbose_name
第二步:定义serializer.py序列化组件

这里我图省事,直接用了ModelSerializer类
在这里插入图片描述

from rest_framework.serializers import Serializer, ModelSerializer
from rest_app01.models.school import School


class SchoolSerializer(ModelSerializer):
    class Meta:
        model = School
        fields = '__all__'
第三步:定义类视图

这里因为真实场景下查询操作必然是会设计到查询所有数据和查询指定数据的场景,所以在类视图的
设计上必然要考虑操作所有和操作指定对象的场景,这里设计了两个视图SchoolListView和SchoolDetailView
在这里插入图片描述
具体代码:

from rest_app01.serializer.serializer import SchoolSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_app01.models.school import School
from django.http import HttpResponse
from django.shortcuts import render, redirect, reverse


class SchoolListView(APIView):
    def get(self, request, *args, **kwargs):
        """
        查询学校列表接口
        """
        # queryset类型的数据集
        school_list = School.objects.all()
        # queryset类型数据序列化成
        ss = SchoolSerializer(school_list, many=True)
        return Response(ss.data)

    def post(self, request, *args, **kwargs):
        """
        新增学校接口
        """
        ss = SchoolSerializer(data=request.data, many=False, context={'request': request})
        if ss.is_valid():
            ss.save()
            return Response(ss.data)
        else:
            return HttpResponse(ss.errors)

         
class SchoolDetailView(APIView):
    def get(self, request, pk, *args, **kwargs):
        """
        查询指定学校列表接口
        """
        try:
            school = School.objects.get(pk=pk)
            ss = SchoolSerializer(school, many=False, context={'request': request})
            return Response(ss.data)
        except School.DoesNotExist as e:
            return HttpResponse('学校不存在')

    def put(self, request, pk, *args, **kwargs):
        """
        修改指定学校信息接口
        """
        try:
            school = School.objects.get(pk=pk)
            ss = SchoolSerializer(school, data=request.data)
            if ss.is_valid():
                ss.save()
                return Response(ss.data)
            else:
                return HttpResponse(ss.errors)
        except School.DoesNotExist as e:
            return HttpResponse('学校不存在')

    def delete(self, request, pk, *args, **kwargs):
        try:
            School.objects.get(pk=pk).delete()
            if School.objects.get(pk=pk):
                return HttpResponse('删除成功')
            else:
                return HttpResponse('删除失败')
        except School.DoesNotExist as e:
            return HttpResponse('学校不存在')
第四步:路由注册

在应用下新建路由文件urls.py或者直接在根路由下注册路由,我这里先在根路径下注册了app的路由,又在app路由下进行应用路由注册。
根路由文件注册应用路由:
在这里插入图片描述

from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('restApp01/', include(('rest_app01.router.urls', 'rest_app01'), namespace='restApp01')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

应用路由文件这里类视图:
在这里插入图片描述

from django.urls import path
from rest_app01.views import school



urlpatterns = [
    # rest接口v1.0.0版本url设置
    path('schools', school.SchoolListView.as_view(), name='schools'),
    path('schools/<int:pk>', school.SchoolDetailView.as_view(), name='schoolDetail'),
]
第五步:设置settings.py文件,迁移模型类,启动服务

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
设置上传文件默认保存位置
在这里插入图片描述迁移模型类 python manage.py makemigratons && python manage.py migrate

启动服务:python manage.py runserver

实例运行结果如下:
在这里插入图片描述
在这里插入图片描述

第二部分 执行流程源码解读

2.1 从路由注册请求分发开始
from django.urls import path
from rest_app01.views import school

urlpatterns = [
    # rest接口v1.0.0版本url设置
    path('schools', school.SchoolListView.as_view(), name='schools'),
    path('schools/<int:pk>', school.SchoolDetailView.as_view(), name='schoolDetail'),
]

在这里插入图片描述
url请求地址为xx/schools时, 之前使用函数视图进行路由注册时,这里传递的参数是一个函数引用,所以school.SchoolListView.as_view()的返回值必然也是一个函数引用。那我们接着看看school.SchoolListView.as_view()的返回值到底是什么?

2.2 as_view()方法到底返回的是什么?

我们到school.py下的SchoolListView类下面找找as_view方法,看看到底执行了什么?
在这里插入图片描述
发现SchoolListView类下压根就没有as_view方法,怎么肥事?这里我们看到SchoolListView有一个父类,既然子类中没有该方法,那我们就到父类中去找,果然在父类APIView中找到了as_view方法,看看as_view的返回结果是不是一个函数引用?
在这里插入图片描述
通过源码我们可以看到APIView类的as_view方法继承了父类的as_view方法并进行了重写,继承了父类的as_view方法返回了一个view,我们在看看在APIView的父类对象View中的as_view方法到底返回了什么?返回值是什么类型?

2.3 APIView类的父类View中的as_view方法到底返回了什么?

在这里插入图片描述
在这里插入图片描述
上面两张截图就是View类中的as_view方法,我们看到as_view方法中定义了一个函数view, as_view最后的返回值就是返回这个函数引用view,大家发现了什么?是不是?。。。 闭包,对就是闭包,返回一个函数引用,这里刚好印证了我们在2.1中讲到的path()函数在进行路由注册时要求第二个参数必须是一个函数引用。

2.4 资源路径和函数引用的一一对应后,那不同的请求方法又是怎么在这同一个view函数上进行请求分发的呢?

在这里插入图片描述
在这里插入图片描述
通过之前的代码通读我们已经知道了school.SchoolListView.as_view()实际上指向的就是view函数,当用户访问当前的url时,view函数自动被调用执行,那我们再看看view函数到底干了什么,返回值是什么?
APIView类的父类View中的as_view方法中的view函数如下,我们一起来看看:

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)

这里我们看到view函数的返回值是self.dispatch(request, *args, **kwargs),这里的self指向得是谁?
这个很重要!!!,一定要搞清楚,谁调用了这个函数,self就指向谁,这里是schools.SchoolListView.as_view()调用的,所以self指向的是SchoolListView类
我们到SchoolListView找dispatch方法,发现没有该方法;没有该方法怎么办?
说明该方法是继承过来的,那就到父类里面找,SchoolListView的父类APIView类中有dispatch方法,我们看看这个方法都做了什么事情,

    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response

在这里插入图片描述
在这里插入图片描述
View类中定义了一个全局变量就是http_method_names ,全是http请求常用的方法;
dispatch函数中解析了request.method,因为request.method返回值是一个全大写的字符串(GET,POST,PUT,DELETE),这里使用lower()全部转成小写,并且进行了判断;
如果request.method在http_method_names的列表中,将request.method处理后的取值赋值给handler变量,最后返回handler()的执行结果,相当于如果 handler=‘get’,返回get(request, *args, **kwargs)的结果,这里就是返回我们在SchoolListView中定义的post和get方法的结果数据,其他的请求类型原理相同,用反射实现了用户请求资源方法类型和对应的请求类型视图函数的一一对应。

第三部分 类视图v1.1.0版本

3.1 示例代码

第一部分的代码有很多的冗余代码,如果类视图的数量一多,重复的代码也会越来越多,一起来看看那rest_framework是怎么一步一步进行封装处理的
views/SchoolListView和views/SchoolDetailView,视图代码如下:

from rest_app01.serializer.serializer import SchoolSerializer
from rest_app01.models.school import School
from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin
from rest_framework.generics import GenericAPIView
"""
rest_framework开发接口v1.1.0版本
主要优化:
    通过引入rest_framework.mixins混合视图组件,将get、post方法中对于数据的处理过程进行封装;
    继承该类视图,传递数据和序列化组件参数即可
"""


class SchoolListView(ListModelMixin, CreateModelMixin, GenericAPIView):
    # queryset类型的数据集
    queryset = School.objects.all()
    # queryset类型数据序列化成
    serializer_class = SchoolSerializer

    def get(self, request, *args, **kwargs):
        """
        查询学校列表接口
        """
        return self.list(self, request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        """
        新增学校接口
        """
        return self.create(self, request, *args, **kwargs)


         
class SchoolDetailView(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericAPIView):
    # queryset类型的数据集
    queryset = School.objects.all()
    # queryset类型数据序列化
    serializer_class = SchoolSerializer

    def get(self, request, *args, **kwargs):
        """
        查询指定学校列表接口
        """
        return self.retrieve(self, request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        """
        修改指定学校信息接口
        """
        return self.update(self, request, *args, **kwargs)

    def delete(self, request, pk, *args, **kwargs):
        """
        删除指定学校信息接口
        """
        return self.destory(self, request, *args, **kwargs)

注意: queryset和serializer_class这两个参数的名字是确定的,只能是这两个参数名称,不能使用别的。

3.1 代码解读

这里的核心在于对于处理全部数据、增加数据、处理单条数据的业务场景分别进行了封装,比如:获取全部的模型数据,这里专门封装了ListModelMixin, CreateModelMixin, GenericAPIView类
在这里插入图片描述
获取全部数据的get()方法返回结果时调用list()得到的返回数据,但SchoolListView中没有list方法,那该方法必然来自于父类,我们就在ListModelMixin, CreateModelMixin, GenericAPIView类中找list方法;
在这里插入图片描述
这里可以看到list方法的返回结果跟我们基础版中的返回结果是一样的,都是返回queryset序列化之后的数据,但是在处理过程中调用了self.filter_queryset(self.get_queryset())和self.get_serializer(queryset, many=True)方法,我们发现SchoolListView、ListModelMixin, CreateModelMixin都没有该方法,在GenericAPIView中有该方法,可能就会有小伙伴有疑问,虽然GenericAPIView和ListModelMixin, CreateModelMixin同级,都是SchoolListView的父类,但是GenericAPIView和ListModelMixin, CreateModelMixin没有继承关系啊?为什么在ListModelMixin和CreateModelMixin中可以调用GenericAPIView中的方法呢?

关于这个疑问,我只想说,你可能对于self这个对象的指向理解的还不是特别清楚。

当SchoolListView中调用self.list方法时,虽然用的是list方法属于父类,但是self指向的永远是当前的调用者,当前的调用者是SchoolListView类,所以self指向的就是SchoolListView类。

当在父类ListModelMixin的list方法中调用self.filter_queryset方法时,因为SchoolListView类中没有filter_queryset方法,那就会从它的父类中找,而ListModelMixin和CreateModelMixin和都没有,而GenericAPIView有,GenericAPIView类中的该方法被SchoolListView类继承。所以self.filter_queryset()实际上执行的是SchoolListView类继承自GenericAPIView的方法。

大家一定要彻底的弄明白面向对象编程的三大核心思想:封装、继承、多态。如果这个理解的不清楚,看源码会很吃力的。

第四部分 类视图v1.1.1版本

4.1 示例代码

第三部分的代码仍然有很多冗余代码,仍然有很多的get/post/put/delete重复代码,我们接着来看看rest-framework是怎么进一步的进行代码封装的。
views/SchoolListView和views/SchoolDetailView,视图代码如下:

from rest_app01.serializer.serializer import SchoolSerializer
from rest_app01.models.school import School
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView


"""
rest_framework开发接口v1.2.0版本
主要优化:
    通过使用mixin类,我们使用更少的代码重写了这些视图,但我们还可以再进一步。
    REST框架提供了一组已经混合好(mixed-in)的通用视图,我们可以使用它来简化我们的views.py模块。
    from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView

"""


class SchoolListView(ListCreateAPIView):
    # queryset类型的数据集
    queryset = School.objects.all()
    # queryset类型数据序列化成
    serializer_class = SchoolSerializer

         
class SchoolDetailView(RetrieveUpdateDestroyAPIView):
    # queryset类型的数据集
    queryset = School.objects.all()
    # queryset类型数据序列化
    serializer_class = SchoolSerializer

注意: queryset和serializer_class这两个参数的名字是确定的,只能是这两个参数名称,不能使用别的。

4.1 代码解读

这里的核心在于两个类 ListCreateAPIView, RetrieveUpdateDestroyAPIView,这里主要旧这两个类进行解读。
在这里插入图片描述
这里只传递了两个参数,一个queryset数据集,一个序列化器,很显然,所有的过程代码都被封装到ListCreateAPIView类中进行处理了,来看看ListCreateAPIView的源码
在这里插入图片描述
大家看,是不是很熟悉,这不就是我们写的第二个版本的类视图代码麽,这里就不多进行再赘述了。

另外在额外说一点儿东西,我在看源码的时候发现rest_framework.generics.py文件下面还有这些将没一种请求方法单独处理的类视图ListAPIView、CreateAPIView、RetrieveAPIView、DestroyAPIView、UpdateAPIView,大家如果不喜欢我的那种继承方式,可以使用多继承的方式进行类继承也是可以的。
毕竟萝卜白菜,各有所爱么。

在这里插入图片描述

第五部分 类视图v2.0.0版本

个人认为最为高级的版本,代码最为精简,如果大家还没有完全掌握rest_framework的封装思想,个人不建议使用这种方式。

5.1 示例代码

第四部分的代码虽然已经很精简了,但是还是不够好用,为什么呢?我们命名是要对同一个数据模型进行操作,确要定义两个视图来进行区别处理,这怎么算精简?有没有什么办法将业务场景【处理所有的数据还是处理指定的数据】的判断也封装到过程代码里面,执行处理还是在同一个类视图中进行呢?
答案肯定是有的,一起来看看

视图文件:

from rest_app01.serializer.serializer import SchoolSerializer
from rest_app01.models.school import School
from rest_framework.viewsets import ModelViewSet


"""
rest_framework开发接口v1.2.0版本
主要优化:


"""


class SchoolViewSet(ModelViewSet):
    # queryset类型的数据集
    queryset = School.objects.all()
    # queryset类型数据序列化成
    serializer_class = SchoolSerializer

注意: queryset和serializer_class这两个参数的名字是确定的,只能是这两个参数名称,不能使用别的。
路由文件urls.py,需要在as_view()方法中传递字典参数,且只能这样设置,如果你想改,先改源码吧!

from django.urls import path
from rest_app01.views import school_v3


urlpatterns = [
    # rest接口v2.0.0版本url设置
    path('v3/schools', school_v3.SchoolViewSet.as_view({
            'get': 'list', 
            'post': 'create'
        }), name='school_v3'),
    path('v3/schools/<int:pk>', school_v3.SchoolViewSet.as_view({
            'get': 'retrieve', 
            'put': 'update', 
            'delete': 'destroy'
        }), name='schoolDetail_v3'),
]
5.2 代码解读

在这里插入图片描述
这里的核心在于一个类ModelViewSet,是从rest_framework.viewsets.py模块中引入的,这里主要j就这个类进行解读。
在这里插入图片描述
在这里插入图片描述
这里单把ViewSetMixin类取出来,看看它的源码中都做了什么

class ViewSetMixin:
    """
    This is the magic.

    Overrides `.as_view()` so that it takes an `actions` keyword that performs
    the binding of HTTP methods to actions on the Resource.

    For example, to create a concrete view binding the 'GET' and 'POST' methods
    to the 'list' and 'create' actions...

    view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
    """

    @classonlymethod
    def as_view(cls, actions=None, **initkwargs):
        """
        Because of the way class based views create a closure around the
        instantiated view, we need to totally reimplement `.as_view`,
        and slightly modify the view function that is created and returned.
        """
        # The name and description initkwargs may be explicitly overridden for
        # certain route configurations. eg, names of extra actions.
        cls.name = None
        cls.description = None

        # The suffix initkwarg is reserved for displaying the viewset type.
        # This initkwarg should have no effect if the name is provided.
        # eg. 'List' or 'Instance'.
        cls.suffix = None

        # The detail initkwarg is reserved for introspecting the viewset type.
        cls.detail = None

        # Setting a basename allows a view to reverse its action urls. This
        # value is provided by the router through the initkwargs.
        cls.basename = None

        # actions must not be empty
        if not actions:
            raise TypeError("The `actions` argument must be provided when "
                            "calling `.as_view()` on a ViewSet. For example "
                            "`.as_view({'get': 'list'})`")

        # sanitize keyword arguments
        for key in initkwargs:
            if key in cls.http_method_names:
                raise TypeError("You tried to pass in the %s method name as a "
                                "keyword argument to %s(). Don't do that."
                                % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r" % (
                    cls.__name__, key))

        # name and suffix are mutually exclusive
        if 'name' in initkwargs and 'suffix' in initkwargs:
            raise TypeError("%s() received both `name` and `suffix`, which are "
                            "mutually exclusive arguments." % (cls.__name__))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            # We also store the mapping of request methods to actions,
            # so that we can later set the action attribute.
            # eg. `self.action = 'list'` on an incoming GET request.
            self.action_map = actions

            # Bind methods to actions
            # This is the bit that's different to a standard view
            for method, action in actions.items():
                handler = getattr(self, action)
                setattr(self, method, handler)

            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get

            self.request = request
            self.args = args
            self.kwargs = kwargs

            # And continue as usual
            return self.dispatch(request, *args, **kwargs)

        # take name and docstring from class
        update_wrapper(view, cls, updated=())

        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
        update_wrapper(view, cls.dispatch, assigned=())

        # We need to set these on the view function, so that breadcrumb
        # generation can pick out these bits of information from a
        # resolved URL.
        view.cls = cls
        view.initkwargs = initkwargs
        view.actions = actions
        return csrf_exempt(view)

    def initialize_request(self, request, *args, **kwargs):
        """
        Set the `.action` attribute on the view, depending on the request method.
        """
        request = super().initialize_request(request, *args, **kwargs)
        method = request.method.lower()
        if method == 'options':
            # This is a special case as we always provide handling for the
            # options method in the base `View` class.
            # Unlike the other explicitly defined actions, 'metadata' is implicit.
            self.action = 'metadata'
        else:
            self.action = self.action_map.get(method)
        return request

    def reverse_action(self, url_name, *args, **kwargs):
        """
        Reverse the action for the given `url_name`.
        """
        url_name = '%s-%s' % (self.basename, url_name)
        kwargs.setdefault('request', self.request)

        return reverse(url_name, *args, **kwargs)

    @classmethod
    def get_extra_actions(cls):
        """
        Get the methods that are marked as an extra ViewSet `@action`.
        """
        return [method for _, method in getmembers(cls, _is_extra_action)]

    def get_extra_action_url_map(self):
        """
        Build a map of {names: urls} for the extra actions.

        This method will noop if `detail` was not provided as a view initkwarg.
        """
        action_urls = OrderedDict()

        # exit early if `detail` has not been provided
        if self.detail is None:
            return action_urls

        # filter for the relevant extra actions
        actions = [
            action for action in self.get_extra_actions()
            if action.detail == self.detail
        ]

        for action in actions:
            try:
                url_name = '%s-%s' % (self.basename, action.url_name)
                url = reverse(url_name, self.args, self.kwargs, request=self.request)
                view = self.__class__(**action.kwargs)
                action_urls[view.get_view_name()] = url
            except NoReverseMatch:
                pass  # URL requires additional arguments, ignore

        return action_urls

首先,ViewSetMixin类中定义了as_view方法,因为SchoolViewSet继承ModelViewSet,ModelViewSet继承GenericViewSet,GenericViewSet继承ViewSetMixin
在这里插入图片描述
路由中 school_v3.SchoolViewSet.as_view()函数必然执行的就是SchoolViewSet从ViewSetMixin类中继承的as_view方法,我们看看这个as_view方法,这个as_view函数接收一个actions的参数,而且规定了当as_view函数被一个ViewSet类调用的时候必须要进行参数传递,传递的参数类型必须是一个字典如({‘get’: ‘list’}),否则就会抛异常。返回值仍然是一个view函数引用。

说白了这个as_view函数还是一个闭包,最后返回一个名为view的函数引用。

那我们再继续看看view函数中又做了什么事情:
在这里插入图片描述
在这里插入图片描述
self.dispatch(request, *args, **kwargs),实际上就是调用SchoolViewSet中的dispatch方法,但是该类中没有;
SchoolViewSet继承ModelViewSet,在ModelViewSet类中也没有dispatch方法,继续;
ModelViewSet继承GenericViewSet,在GenericViewSet类中也没有dispatch方法,继续;
GenericViewSet继承ViewSetMixin和generics.GenericAPIView,在ViewSetMixin类中也没有dispatch方法,就在generics.GenericAPIView类中找,也没有,
但是GenericAPIView类继承views.APIView类,就继续在views.APIView类中找,还在APIView类中找到了dispatch方法,执行该方法
在这里插入图片描述
但是SchoolViewSet类中没有list方法,没关系,往父类ModelViewSet里面找呗
在这里插入图片描述

ModelViewSet类不是继承了mixins.ListModelMixin类麽,看看这个类里面是不是有个list方法,返回值是不是就是一个已经序列化过的queryset数据集
在这里插入图片描述
其他的请求类型执行的原理跟上述相同,不赘述。OK,至此rest_framework框架类视图中反射不同类型的http请求方法的所有版本的原理和过程我基本上都写下来了。
本文纯属个人理解,如有理解错误或阐述不清,请多多理解啦!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值