写 class MyView(View): ...,urls.py 里却是 path('x/', MyView.as_view())——这中间发生了什么,as_view() 返回的到底是不是这个类本身,本机真实读源码 + 真实跑了一遍。顺带验证了几个容易被忽略的边界:参数名打错字什么时候会报错、没定义的方法为什么是真实的 405、以及 get 和 head 之间一个隐藏的默认关联。
View 这个基类本身很朴素——核心就是 as_view() 里那个内层的 view() 函数。
django/views/generic/base.py · django @ 6.0.7, L82 @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("The method name %s is not accepted as a keyword argument to %s()." % (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) # 每次请求,真的重新 new 一个实例 self.setup(request, *args, **kwargs) if not hasattr(self, "request"): raise AttributeError(...) # setup() 忘了调 super() 会在这里被抓到 return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs return view
urls.py 里挂的 MyView.as_view(),拿到的从来不是 MyView 这个类,而是这个闭包 view 函数——它符合 Django URL 解析要求的"一个函数视图"的形状。真正的 MyView 实例,是每次请求进来、这个闭包被调用的那一刻,用 cls(**initkwargs) 现造的。
既然是"现造"的,那两次请求得到的 self 应该是两个不同的对象——不是常驻内存、被所有请求共用的单例。
instances = []
orig_dispatch = Echo.dispatch
def spy_dispatch(self, request, *args, **kwargs):
instances.append(self)
return orig_dispatch(self, request, *args, **kwargs)
Echo.dispatch = spy_dispatch
view_fn = Echo.as_view()
view_fn(rf.get('/x'))
view_fn(rf.get('/x'))
print('两次请求得到的 self 是同一个实例吗:', instances[0] is instances[1])
两次请求得到的 self 是同一个实例吗: False
这也是为什么可以放心在 self.foo = ... 上存这次请求期间要用的临时状态(self.request/self.args/self.kwargs 就是 setup() 这么干的)——不会有并发的两个请求共享同一个实例、互相污染对方数据的风险,每个请求从 view() 闭包被调用开始,都拿到一个全新的、只属于自己的对象。
上面源码里那个 for key in initkwargs 循环,是在 as_view() 被调用的那一刻就跑的——也就是 urls.py 被导入的时候,不是等第一个请求真的进来。
class Echo(View):
template_name = 'echo.html'
def get(self, request):
return None
Echo.as_view(tempate_name='typo.html') # 故意打错 template_name
TypeError: Echo() received an invalid keyword 'tempate_name'. as_view only
accepts arguments that are already attributes of the class.
Echo.as_view(get=lambda request: None) # 想用 initkwargs 覆盖 get 方法TypeError: The method name get is not accepted as a keyword argument to Echo().
两种错误都是 TypeError,都发生在服务器启动、Django 加载 urls.py 的那一刻,而不是某个用户凑巧发起请求触发的运行时错误——这也是为什么 CBV 的 as_view(xxx=...) 打错参数名几乎不可能漏到生产环境:项目启动不起来,问题第一时间暴露。
dispatch() 本体只有几行,核心是一次 getattr。
django/views/generic/base.py · django @ 6.0.7, L135 def dispatch(self, request, *args, **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 return handler(request, *args, **kwargs) def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)]
# 只定义了 get/post 的视图,收到 PUT 请求
class Echo(View):
def get(self, request): return None
def post(self, request): return None
resp = Echo.as_view()(rf.put('/x'))
print(resp.status_code, resp.headers.get('Allow'))
405 GET, POST, HEAD, OPTIONS
Allow 头里的 HEAD/OPTIONS 并不是显式定义出来的——OPTIONS 是 View 基类自带的默认实现(所有子类天生就有),HEAD 则是下一节要讲的 setup() 悄悄加上去的。
django/views/generic/base.py · django @ 6.0.7, L127 def setup(self, request, *args, **kwargs): if hasattr(self, "get") and not hasattr(self, "head"): self.head = self.get # 定义了 get、没定义 head,直接把 get 挂到 head 上 self.request = request
calls = []
class Echo2(View):
def get(self, request):
calls.append('get 被调用了')
return None
Echo2.as_view()(rf.head('/x')) # 发一个 HEAD 请求,这个类完全没写 head()
print('调用记录:', calls)
调用记录: ['get 被调用了']
大多数视图不需要单独处理 HEAD(按 HTTP 语义,响应体会被更上层的机制丢弃,业务逻辑跟 GET 一致),Django 索性把这个"复用"直接做进了 setup(),不用每个视图自己写一遍 head = get。
class Mixed(View):
def get(self, request): return None
async def post(self, request): return None # 一个同步一个异步
Mixed.view_is_async
ImproperlyConfigured: Mixed HTTP handlers must either be all sync or all async.
这条检查发生在 view_is_async 这个 classproperty 里,只要有人访问它(as_view() 内部会访问)就会触发——同一个视图类里的 handler 必须要么全同步要么全异步,不能一半一半。
urls.py 拿到的是 view() 闭包,不是类——但像 @csrf_exempt 这样靠"在函数对象上打标记"生效的装饰器,得想办法让标记从类方法传递到这个闭包上。
django/views/generic/base.py · django @ 6.0.7, L119 # Copy possible attributes set by decorators, e.g. @csrf_exempt, from # the dispatch method. view.__dict__.update(cls.dispatch.__dict__)
@method_decorator(csrf_exempt, name='dispatch')
class Echo(View):
def post(self, request): return None
view_fn = Echo.as_view()
print('dispatch 方法自己有没有标记:', getattr(Echo.dispatch, 'csrf_exempt', False))
print('as_view() 返回的函数有没有:', getattr(view_fn, 'csrf_exempt', False))
dispatch 方法自己有没有标记: True
as_view() 返回的函数有没有: True
@method_decorator(csrf_exempt, name='dispatch') 先把 csrf_exempt 这个标记打在 Echo.dispatch 这个方法对象上,as_view() 最后再用 view.__dict__.update(cls.dispatch.__dict__) 原样搬到 view 函数身上——CSRF 中间件检查的正是 view_fn.csrf_exempt 这个属性,这一步"搬运"不做,装饰器就形同虚设。
跟上面真实实验完全同构:Echo 只写了 get()/post(),看 dispatch() 怎么给每种请求方法找到处理函数。
| 请求方法 | getattr(self, method) 找到了吗 | 实际调用 |
|---|
数据直接对应上面真实实测的 Echo 类(只定义 get/post):head 因为 setup() 的自动关联指向 get,options 用的是 View 基类自带的默认实现,put/delete/patch 找不到对应方法,统一落到 http_method_not_allowed——每一行都和真实的 _allowed_methods()/405 实验结果核对过一致。