Django的View类是如何工作的?
我正在研究Django的通用视图,想弄清楚它们是如何返回一个简单的HttpResponse对象的,就像普通的视图函数一样。
我写了一个简单的项目来测试,并在django/views/generic/base.py文件中定义的基本视图类里添加了一些日志命令,这样我就能跟踪背后发生了什么。
在研究过程中,我有一些问题。
我尽量让这篇帖子简短,不过为了更好地理解,我觉得有必要包含一些代码片段和日志。
如果有人能花时间给我一些有用的评论,可能回答我的一些问题,我将非常感激。
urls.py
from django.conf.urls import patterns, url
from views import WelcomeView
urlpatterns = patterns('',
url(r'^welcome/(?P<name>\w+)/$', WelcomeView.as_view()),
)
views.py
from django.http import HttpResponse
from django.views.generic import View
class WelcomeView(View):
def get(self, request, name):
return HttpResponse('What is up, {0}?'.format(name))
django/views/generic/base.py
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', 'delete', 'head', 'options', 'trace']
def __init__(self, **kwargs):
#####logging
logging.error('*** View.__init__ is started with kwargs: {0} ***'.format(
repr(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 kwargs.iteritems():
setattr(self, key, value)
#####logging
logging.error('*** View.__init__ reached its end. No return value. ***')
@classonlymethod
def as_view(cls, **initkwargs):
#####logging
logging.error('*** View.as_view is started with initkwargs: {0} ***'.format(
repr(initkwargs)))
"""
Main entry point for a request-response process.
"""
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError(u"You tried to pass in the %s method name as a "
u"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError(u"%s() received an invalid keyword %r" % (
cls.__name__, key))
def view(request, *args, **kwargs):
#####logging
logging.error('*** View.as_view.view is called with args: {0};\
and kwargs: {1} ***'.format(
repr(args),
repr(kwargs)))
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
#####logging
logging.error('*** View.as_view.view reached its end.\
Now calls dispatch() and returns the return value.')
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=())
#####logging
logging.error('*** View.as_view reached its end. Now returns view. ***')
return view
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.
#####logging
logging.error('*** View.dispatch called, with args: {0};\
and kwargs: {1} ***'.format(
repr(args),
repr(kwargs)))
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
self.request = request
self.args = args
self.kwargs = kwargs
#####logging
logging.error('*** View.dispatch reached its end.\
Now calls handler and returns the return value. ***')
return handler(request, *args, **kwargs)
def http_method_not_allowed(self, request, *args, **kwargs):
allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
logger.warning('Method Not Allowed (%s): %s', request.method, request.path,
extra={
'status_code': 405,
'request': self.request
}
)
return http.HttpResponseNotAllowed(allowed_methods)
一些测试请求的日志
Django version 1.4.5, using settings 'try1.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
ERROR:root:*** View.as_view is started with initkwargs: {} ***
ERROR:root:*** View.as_view reached its end. Now returns view. ***
ERROR:root:*** View.as_view.view is called with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.__init__ is started with kwargs: {} ***
ERROR:root:*** View.__init__ reached its end. No return value. ***
ERROR:root:*** View.as_view.view reached its end. Now calls dispatch() and returns the return value.
ERROR:root:*** View.dispatch called, with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.dispatch reached its end. Now calls handler and returns the return value. ***
[24/Feb/2013 12:43:19] "GET /welcome/Dude/ HTTP/1.1" 200 17
ERROR:root:*** View.as_view.view is called with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.__init__ is started with kwargs: {} ***
ERROR:root:*** View.__init__ reached its end. No return value. ***
ERROR:root:*** View.as_view.view reached its end. Now calls dispatch() and returns the return value.
ERROR:root:*** View.dispatch called, with args: (); and kwargs: {'name': u'Dude'} ***
ERROR:root:*** View.dispatch reached its end. Now calls handler and returns the return value. ***
[24/Feb/2013 12:43:32] "GET /welcome/Dude/ HTTP/1.1" 200 17
[24/Feb/2013 12:44:43] "GET /welcome/ HTTP/1.1" 404 1939
ERROR:root:*** View.as_view.view is called with args: (); and kwargs: {'name': u'Bro'} ***
ERROR:root:*** View.__init__ is started with kwargs: {} ***
ERROR:root:*** View.__init__ reached its end. No return value. ***
ERROR:root:*** View.as_view.view reached its end. Now calls dispatch() and returns the return value.
ERROR:root:*** View.dispatch called, with args: (); and kwargs: {'name': u'Bro'} ***
ERROR:root:*** View.dispatch reached its end. Now calls handler and returns the return value. ***
[24/Feb/2013 12:44:59] "GET /welcome/Bro/ HTTP/1.1" 200 16
最后,我的问题
1.
根据日志,as_view在View.init之前被调用。
这是否意味着它在创建View实例之前就调用了View的方法?
2.
为什么在第一次调用后不再调用as_view()?
我还不是Python导入、编译和内存使用方面的专家,
但我感觉这些可能在这里起到了一些作用。
3.
在view()的定义中,以下代码片段是做什么的?
self = cls(**initkwargs)
根据日志,它触发了View.init。
这是否意味着它用initkwargs创建了一个新的View实例,并将其分配给正在使用的实例(self)?
如果是这样,为什么需要这样做?
4.
我们如何利用initkwargs(as_view的参数)?
2 个回答
1. 首先,as_view() 是一个 类方法。这意味着你可以在类上调用这个方法,而不是在类的实例上。在这里,你可以看到它是被调用在 View
这个类上,而不是某个实例。
2. 当 url.conf 模块被加载时,会调用 as_view()
,它返回一个叫 view()
的函数。每当请求一个视图时,实际上都是在调用这个函数,而 as_view
不需要再被调用了。
3. 在 view()
函数的作用域内,cls
变量代表的是 View
类(比如 DetailView
、ListView
,或者其他继承自 View
的类)。在类方法中把第一个参数称为 cls
是一种编码风格的规范,来自于 PEP8。这和我们在实例方法中把第一个参数称为 self 是类似的。所以
self = cls(**initkwargs)
基本上和
self = View(**initkwargs)
或 self = DetailView(**initkwargs)
(具体取决于哪个类在调用这个函数)是一样的。这就是你所说的,正在创建这个类的新实例。在这之前,View
对象还没有被实例化。
4. 最后,initkwargs 是在创建类的实例时使用的。其实就是把每个键值对作为新视图对象的属性添加进去,简单来说就是这样 -
for key, value in kwargs.iteritems():
setattr(self, key, value)
这些视图的底层实现涉及一些比较复杂的Python代码,所以如果你是刚入门的编程小白,看到这些代码感到困惑也是很正常的。
首先,你需要明白的是,
@classmethod
这个装饰器在as_view()
方法定义上有什么作用。它的意思是,这个方法不是一个普通的方法,普通方法是通过类的实例来调用的(并且会把这个实例作为self
参数),而是一个类方法,它是直接在类本身上调用的(并且会把类作为cls
参数)。有些编程语言把这个叫做静态方法,不过在Python中还有一种第三种方法,这里就不详细讲了。这是因为视图是如何在urlconf中定义的。你正确地写了
WelcomeView.as_view()
- 这行代码的作用是在导入urlconf的时候调用as_view
这个类方法。正如我们在第一点中提到的,
cls
就是视图类本身。像普通类一样,当你调用它时,会得到一个对象。所以,正如你所说的,我们在这里是实例化这个类,然后把这个实例赋值给一个叫self
的变量,就好像我们在这个实例的方法里一样。这里的关键是,正如我之前提到的,as_view
是在导入时被调用的,它返回一个函数 -view
- 这个函数会在浏览器请求这个URL时被URL调度器调用。所以在这个函数内部,我们构建并调用组成基于类的视图的其余部分。至于为什么需要这样做,下面会解释。__init__
方法负责将每个initargs
的成员设置为实例属性,你可以通过通常的self.whatever
语法在视图代码中访问它。
那么,为什么这一切都是必要的呢?
基于类的视图有一个很大的潜在问题,那就是任何在URLconf中(或模块级别的其他地方)直接实例化的类会在整个进程的生命周期内持续存在。而Django通常的部署方式是通过WSGI,这通常意味着一个进程可以处理很多请求。如果你有一些东西在多个请求之间持续存在,就可能会出现一些非常棘手的线程安全问题 - 比如,如果你在一个请求中把某个东西设置为实例属性,那么在后续的请求中它也会可见。
所以,这段代码不仅确保每个请求都会得到一个新的实例,还通过在视图函数内部动态构建实例,使得打破这种请求隔离变得非常困难。