Django的View类是如何工作的?

15 投票
2 回答
6596 浏览
提问于 2025-04-17 16:57

我正在研究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 个回答

2

1. 首先,as_view() 是一个 类方法。这意味着你可以在类上调用这个方法,而不是在类的实例上。在这里,你可以看到它是被调用在 View 这个类上,而不是某个实例。

2. 当 url.conf 模块被加载时,会调用 as_view(),它返回一个叫 view() 的函数。每当请求一个视图时,实际上都是在调用这个函数,而 as_view 不需要再被调用了。

3. 在 view() 函数的作用域内,cls 变量代表的是 View 类(比如 DetailViewListView,或者其他继承自 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)
23

这些视图的底层实现涉及一些比较复杂的Python代码,所以如果你是刚入门的编程小白,看到这些代码感到困惑也是很正常的。

  1. 首先,你需要明白的是,@classmethod这个装饰器在as_view()方法定义上有什么作用。它的意思是,这个方法不是一个普通的方法,普通方法是通过类的实例来调用的(并且会把这个实例作为self参数),而是一个类方法,它是直接在类本身上调用的(并且会把作为cls参数)。有些编程语言把这个叫做静态方法,不过在Python中还有一种第三种方法,这里就不详细讲了。

  2. 这是因为视图是如何在urlconf中定义的。你正确地写了WelcomeView.as_view() - 这行代码的作用是在导入urlconf的时候调用as_view这个类方法。

  3. 正如我们在第一点中提到的,cls就是视图类本身。像普通类一样,当你调用它时,会得到一个对象。所以,正如你所说的,我们在这里是实例化这个类,然后把这个实例赋值给一个叫self的变量,就好像我们在这个实例的方法里一样。这里的关键是,正如我之前提到的,as_view是在导入时被调用的,它返回一个函数 - view - 这个函数会在浏览器请求这个URL时被URL调度器调用。所以在这个函数内部,我们构建并调用组成基于类的视图的其余部分。至于为什么需要这样做,下面会解释。

  4. __init__方法负责将每个initargs的成员设置为实例属性,你可以通过通常的self.whatever语法在视图代码中访问它。

那么,为什么这一切都是必要的呢?

基于类的视图有一个很大的潜在问题,那就是任何在URLconf中(或模块级别的其他地方)直接实例化的类会在整个进程的生命周期内持续存在。而Django通常的部署方式是通过WSGI,这通常意味着一个进程可以处理很多请求。如果你有一些东西在多个请求之间持续存在,就可能会出现一些非常棘手的线程安全问题 - 比如,如果你在一个请求中把某个东西设置为实例属性,那么在后续的请求中它也会可见。

所以,这段代码不仅确保每个请求都会得到一个新的实例,还通过在视图函数内部动态构建实例,使得打破这种请求隔离变得非常困难。

撰写回答