在 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
定义的更加具体的通用类视图。
如果遇到问题,请通过下面的方式寻求帮助。
- 在下方评论区留言。
- 在 Pythonzhcn 社区的新手问答版块 发布帖子。
更多 Django 相关教程,请访问我的个人博客:追梦人物的博客。
-- EOF --
我想问下关于update_wrapper(),为什么需要这个函数以及具体的作用。
期待着这部分的后序更新,很有收获。
看注释:# take name and docstring from class
不必在意其细节,只是为了复制被装饰的类名和文档字符串而已。
zhui adsfasf
@classonlymethod
def as_view(cls, **initkwargs): 中的cls,是从那里来的??
类似于实例调用时的 self 参数,会自动传递,指向调用这个方法的类。
再咨询一个问题:
通过,阅读你的教程与查看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的使用,我的大概理解,对不对??
基本上是对的。其实类视图最终也还是通过as_view 方法将类转为函数,类中的的方法转为函数的逻辑。
什么时候更新啊,期待中~~
赞一个
很给力
看完了,很给力,后期会出djangorestframework的教程吗?现在很多公司基本都是前后端分离,感觉直接用django的模板开发的应该很少吧,个人观点。
嗯,有可能。只是目前我还没学到那一步。你可以先看看文档,到时候出一些实战教程。