基类View

2017-08-2911913 阅读12 评论

Class-based views 源码解析 #1 中我们从宏观层面讨论了 Django 类视图的类继承结构以及命名规律。接下来我们要深入各个具体的类视图,探索其具体的代码实现。本节将分析 base.py 中最重要的的一个类,它也是所有类视图的基类 View

之前我们说过,尽管类视图看上去类的种类繁多,但每个类都是各司其职的,且从类的命名就可以很容易地看出这个类的功能。大致可分为如下三个大的功能块,分别由三个类提供对应的方法:

  • 处理 HTTP 请求。根据 HTTP 请求方法的不同做出相应处理。例如同一个视图函数既要处理 get 请求,又要处理 post 请求。这一块的功能由 View 类及其派生类实现。
  • 渲染模板。这一块功能由 TemplateResponseMixin 及其派生类实现。
  • 获取渲染模板所需的模板变量字典(通常称为 context),这个功能由 ContextMixin 及其派生类实现。

现在我们来看看 View 的具体实现,TemplateResponseMixin 以及ContextMixin 将在接下来的系列文章中讲解。

View

Django 类视图的核心就是这个类,这个类是所有其它类视图的基类,它定义所有类视图共有的初始化逻辑,以及一些共有的方法,以便其它类视图继承。始终记住一点,这个类的功能主要是处理不同的 HTTP 请求,因此这个类的属性和方法也是围绕这个功能点设计的。

__init__

先来看看这个类的初始化:

class View(object):
    """
    Intentionally simple parent class for all views. Only implements
    dispatch-by-method and simple sanity checking.
    """

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

    def __init__(self, **kwargs):
        """
        Constructor. Called in the URLconf; can contain helpful extra
        keyword arguments, and other things.
        """
        # Go through keyword arguments, and either save their values to our
        # instance, or raise an error.
        for key, value in six.iteritems(kwargs):
            setattr(self, key, value)

源码中的注释其实已经清楚的说明了这个类的作用。http_method_names 属性记录 HTTP 协议所允许的全部 HTTP 方法。初始化 __init__ 方法非常简单,就是将所有传入的关键字参数 kwargs 通过 setattr(self, key, value) 设置为类实例的属性。

dispatch

接下来是一个重要的方法 dispatch,该方法会根据 HTTP 请求方法的不同而将请求转发给类视图中对应的方法处理,先来看代码实现:

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        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
        return handler(request, *args, **kwargs)

首先它通过 request.method (即 HTTP 请求的方法)判断请求的方法是否是被 HTTP 协议所允许的。如果不合法,就会调用错误处理函数 self.http_method_not_allowed;如果请求方法是合法的,就会试图根据 request.method 去类中寻到对应的处理方法,如果找不到则还是委托给 self.http_method_not_allowed 处理。

可能只看代码有点糊涂,举个例子就能形象地说明 dispatch 方法的处理逻辑。假设 HTTP 请求的方法为 post,则 request.method.lower() == 'post'。此时 dispatch 将尝试调用类视图的 post 方法,并返回 post 方法调用后的值。而如果类视图中没有定义 post 方法(例如现在所说的 View 类中就没有定义),或者请求的方法不是 post 而是 HTTP 协议未规定的方法如 foo,那么 dispatch 就会返回调用 http_method_not_allowed 后的结果。

事实上这个方法的处理逻辑放在视图函数中我们就再熟悉不过了:

def view(request):
    if request.method.lower() == 'get':
        do_something()
    if request.method.lower() == 'post':
        do_something()

http_method_not_allowed

至于上面所说的错误处理方法则非常简单,它的代码如下:

  def http_method_not_allowed(self, request, *args, **kwargs):
      logger.warning(
        'Method Not Allowed (%s): %s', request.method, request.path,
        extra={'status_code': 405, 'request': request}
      )
      return http.HttpResponseNotAllowed(self._allowed_methods())

即立即返回一个 HttpResponseNotAllowed,这一个 HttpResponse 对象,根据 HTTP 规定其状态码为 405,代表不允许的 HTTP 方法。

options

在 HTTP 协议规定的方法中,View 类只实现了一个,即 options方法:

    def options(self, request, *args, **kwargs):
        """
        Handles responding to requests for the OPTIONS HTTP verb.
        """
        response = http.HttpResponse()
        response['Allow'] = ', '.join(self._allowed_methods())
        response['Content-Length'] = '0'
        return response

HTTP 规定客户端使用该方法查询服务器所能处理的全部 HTTP 方法,对任何视图函数来说该方法的逻辑基本是不变的,所以写在了 View 基类中,至于其它需要处理的 HTTP 方法如 post、get 等方法则由 View 的子类根据其具体功能实现。

当然 View 中还有一个辅助方法,就是返回视图类所定义的全部 HTTP 规定的方法。例如在 View 这个类中只定义了 options 方法,所以只会返回 ['options', ]

    def _allowed_methods(self):
        return [m.upper() for m in self.http_method_names if hasattr(self, m)]

as_view

最后剩下一个最重要的方法,即 as_view 方法。如果你曾经使用过类视图,那么最熟悉的应该就是这个方法了。要想让类视图生效,必须在 urls.py 的 URL 模式(Pattern)里做类似如下的配置:

...
urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
]

Django 使用如上的方式配置 URL 到对应视图函数的路由映射。注意到 url() 函数前两个位置参数需要传递的值,第一个是需要捕获的 url 的正则模式,第二个参数则是一个可调用的对象(即视图函数)。如果我们通过 def 定义视图函数,那么传入的这个可调用对象就是这个函数本身;而如果我们定义的是类视图,则必须调用类视图的 as_view 方法返回一个根据这个类生成的可调用对象。类视图所有的魔法就在这个函数里了,来看看 Django 究竟是如何神奇地把一个类转为一个函数的。

    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        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. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

        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.view_class = cls
        view.view_initkwargs = initkwargs

        # 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=())
        return view

as_view 方法被调用时允许传递一些关键字参数,不过需要做一个点点检查,第一防止你传入诸如 get、post 这样的关键字参数把类本身的 get、post 方法覆盖了;第二是防止你传入未定义为类属性的参数。最开始的 for 循环就是做这个事。

接下来在 as_view 方法中又定义了一个 view 方法,这个方法相信如果你经常写视图函数的话应该非常眼熟,这就是视图函数的标准定义:接收一个 HttpRequest 对象,以及从 url 捕获的非命名组和命名组参数。只不过在 view 这个视图函数里还多做了一点事,它首先实例化了一个类视图对象,然后把函数的参数设置为了这个类视图实例的属性,接着便调用了实例的 dispatch 方法返回视图函数被要求返回的 HttpResponse 对象(注意 dispatch 方法会根据 HTTP 请求方法的不同去调用对应的处理方法)。接着把类中的一些文档字符串和函数名等更新到定义的 view 函数中,然后 as_view 方法返回这个 view 函数。

所以回过头来再看一下我们的 url 模式定义:

urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
]

views.IndexView.as_view() 调用后返回的就是一个在 IndexView 里通过 def 定义的视图函数 view(注意所有类视图都继承自 View 基类),是不是和你直接在这里放一个视图函数是一样的?

进一步理解 View 的逻辑

你可能对这个定义在类 View 的方法 as_view 中的函数 view 的逻辑还是不理解,这里我们通过一种分离的实现方式来加深一下对它的理解。我们假设写了如下的一个视图函数

def view(request, *args, **kwargs):
    if request.method.lower() == 'get':
        do_something()
    if request.method.lower() == 'post':
        do_something()

我们很快发现,在很多的视图函数中都复用了这一段代码:

if request.method.lower() == 'get':
    do_something()
if request.method.lower() == 'post':
    do_something()

但是写在函数中的代码复用起来是比较麻烦的,想到代码复用,我们立即想到了类继承,于是我们定义一个辅助类:

class View(object):
    def __init__(request, *args, **kwargs):
        # init

    def get(request, *args, **kwargs)
        do_something()

    def post(request, *args, **kwargs)
        do_something()

让后我们在 view 中实例化这个类并使用它:

def view(request, *args, **kwargs):
    view_instance = View(request, *args, **kwargs)
    if request.method.lower() == 'get':
        view_instance.get(request, *args, **kwargs)
    if request.method.lower() == 'post':
        view_instance.post(request, *args, **kwargs)

可以看到,这个辅助的 View 类就充当了上述所分析的类视图 View 的功能,而这个视图函数 view 则充当了定义在类视图 as_view 方法中的 view 函数的功能。这种设计思想就是把视图函数的逻辑定义到类的方法里面去,然后在函数中实例化这个类,通过调用类的方法实现函数逻辑,而把逻辑定义在类中的一个好处就是可以通过继承复用这些方法。但是像上述这种函数与类分离的实现方式很麻烦且不优雅,直接把 view 定义在类里,就是 Django 类视图的实现方式了。

总结

现在我们已经明白了类视图的基本结构,其主要功能就是根据 HTTP 请求方法的不同做出相应处理,具体的实现为 dispatch 方法。类视图的核心思想就是把视图函数的逻辑定义到类的方法里面去,然后在函数中实例化这个类,通过调用类的方法实现函数逻辑。基类 View 定义了所有类视图的基本逻辑框架,接下来我们会继续分析一系列基于这个基类 View 定义的更加具体的通用类视图。

如果遇到问题,请通过下面的方式寻求帮助。

更多 Django 相关教程,请访问我的个人博客:追梦人物的博客

-- EOF --

12 评论
登录后回复
darker
2019-02-04 13:34:45

我想问下关于update_wrapper(),为什么需要这个函数以及具体的作用。

期待着这部分的后序更新,很有收获。

回复
追梦人物 darker
2019-02-11 12:12:24

看注释:# take name and docstring from class

不必在意其细节,只是为了复制被装饰的类名和文档字符串而已。

回复
海上有个树荫_hhh
2018-04-23 22:53:31

zhui  adsfasf

回复
秋枫鹤泣
2018-04-21 14:56:00

@classonlymethod

def as_view(cls, **initkwargs): 中的cls,是从那里来的??

回复
追梦人物 秋枫鹤泣
2018-04-23 11:48:18

类似于实例调用时的 self 参数,会自动传递,指向调用这个方法的类。

回复
秋枫鹤泣 追梦人物
2018-04-23 16:04:52

再咨询一个问题:

    通过,阅读你的教程与查看base.py源码;我理解class base view是这样的:

    1. 用户访问时,通过路由系统,返回一个as_view中的view函数对象。

    2. 执行 view函数对象 的方法时,会返回一个类本身的dispatch方法

    3. 执行 dispatch方法时,通过getattr方法,最近返回views.py中对应自定类下,定义的http method(get,post)方法。

    4. 最后,会有相应的django框架逻辑方法,执行第3步中返回的(get,post)方法。

以上,是一个最简单的class base view的使用,我的大概理解,对不对??

回复
追梦人物 秋枫鹤泣
2018-04-25 11:19:56

基本上是对的。其实类视图最终也还是通过as_view 方法将类转为函数,类中的的方法转为函数的逻辑。

回复
张旭楠_Python
2018-01-22 17:29:33

什么时候更新啊,期待中~~

回复
boyl
2018-01-02 16:32:39

赞一个

回复
chenzhixiang1992
2017-10-06 18:17:27

很给力

回复
Hogantry
2017-08-30 13:57:07

看完了,很给力,后期会出djangorestframework的教程吗?现在很多公司基本都是前后端分离,感觉直接用django的模板开发的应该很少吧,个人观点。

回复
追梦人物 Hogantry
2017-08-30 14:12:32

嗯,有可能。只是目前我还没学到那一步。你可以先看看文档,到时候出一些实战教程。

回复