基于类的视图或CBV是Django中争议最大的功能之一。与基于功能的视图(FBV)相比,CBV似乎更令人困惑和难以理解。在本系列的Django中的《理解基于类的视图》中,我们将详细介绍CBV,以了解它们的工作方式和使用方法。
首先,我们将介绍View基类,如何在URLconf中使用CBV以及View类如何路由从其继承的其他类的视图逻辑。
先决条件
本文针对那些以前可能尝试使用CBV并希望了解其工作原理的人。如果您满足以下条件,您将从本文中获得最大收益:
之前已经用Django建立了一个项目
尝试至少使用一次CBV
对Python中的类有基本的了解
本文包含许多不完整的代码段,用于说明目的。如果您以前使用过Django,那么了解这些片段在项目中的适合位置就不难了。我尝试添加尽可能多的上下文,但是省略了许多支持代码,以使文章的长度易于理解。
CBV的调用方式
让我们开始看看与FBV相比,我们如何在URLconf中使用CBV。 假设在项目的urls.py文件中有一个名为MyView的CBV和一个名为my_view的FBV,我们在path()函数中将它们用作urlpatterns的一部分。
1 2 3 4 5 |
# Class-Based View path('new-cbv/', MyView.as_view(), name='new_cbv') # Function-Based View path('new-fbv/', my_view, name='new_fbv') |
Django希望path()的第二个参数是一个函数。 这意味着我们可以直接将FBV提供给path()。 我们提供my_view而不是my_view(),因为我们不想调用该函数。 Django稍后会调用它并适当地使用它。
使用CBV是不同的。 最初,我们可能希望可以将MyView类直接传递给path()。
1 2 |
# Wrong way to use a CBV in the URLconf path('new-cbv/', MyView, name='new_cbv') |
但是,这将不起作用,因为path()不希望将类用作视图的参数。 我们需要以某种方式从类中获取一个函数。 我们通过在MyView上调用.as_view()来实现。 但是使用MyView.as_view()时,我们正在调用一个函数,而不是像使用FBV那样直接传递一个函数。 我们尚不知道.as_view()返回什么,但是,如果path()需要一个函数,则.as_view()必须返回一个。 为了找出返回的结果,我们必须深入研究Django的内置View基类。
深入View类
所有CBV都从View继承为其基类。 CBV可能继承自许多不同的类和mixin,但它们都以View开头。 让我们直接从Django源代码中查看View背后的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
class View: """ 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 kwargs.items(): setattr(self, key, value) @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) self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) 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 def setup(self, request, *args, **kwargs): """Initialize attributes shared by all view methods.""" if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs 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) 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 HttpResponseNotAllowed(self._allowed_methods()) def options(self, request, *args, **kwargs): """Handle responding to requests for the OPTIONS HTTP verb.""" response = HttpResponse() response['Allow'] = ', '.join(self._allowed_methods()) response['Content-Length'] = '0' return response def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)] |
这里有很多事情要做,但是我们将以Django从.as_view()开始的方式来完成它。
as_view()方法
由于我们在URLconf中调用.as_view(),因此是在创建类的实例之前调用View的第一部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
@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) self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) 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 |
@classonlymethod装饰器与.as_view()一起使用,以确保未在类的实例上调用它,而是仅在类上直接调用它。 现在是指出使用CBV时不直接创建类实例的好时机。
1 |
new_view = View() # You won't do this |
取而代之的是,稍后会由于.as_view()而创建一个实例。
.as_view()有两个参数:cls,即类.as_view()被调用并自动传递给该方法,以及** initkwargs。 ** initkwargs是我们在调用.as_view()时传递给它的任何关键字参数,而在最终创建该类的实例时可能需要这些参数。 我们很快就会看到另一组关键字参数,因此请记住,在类实例化期间使用** initkwargs。 如果确实有关键字参数,则可以像这样在URLconf中传递它们:
1 2 |
# Passing example keyword arguments to .as_view path('new-cbv/', MyView.as_view(kwarg1=new_kwarg_1, kwarg2=new_kwarg_2), name='new_cbv') |
调用.as_view()时执行的第一个代码是遍历initkwargs并执行两次检查。
1 2 3 4 5 6 7 8 9 10 |
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)) |
首先对照包含HTTP verbs列表的View类的http_method_names属性检查initkwargs中的每个键。
1 |
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] |
如果我们尝试将HTTP动词作为.as_view()的参数传递,则会出现错误,因为它可能导致视图逻辑的执行出现问题。
还检查每个键以确保它与该类的现有属性匹配。 View类在http_method_names旁边没有类属性,因此让我们快速看一下Django内置的RedirectView CBV的前几行。
1 2 3 4 5 6 |
class RedirectView(View): """Provide a redirect on any GET request.""" permanent = False url = None pattern_name = None query_string = False |
RedirectView具有多个类属性,我们可以通过.as_view()上的关键字参数来设置。 但是,如果我们尝试传递不是列出的关键字参数之一的关键字参数,则会收到错误消息。
检查initkwargs之后,我们进入.as_view()部分,将我们与最初的目标联系起来,以查明.as_view()返回什么,并希望它是一个函数,因为URLconf中的path()需要一个函数。
在.as_view()中定义了一个view()函数,如果我们跳到.as_view()的底部,我们可以看到它返回了此函数。
所以现在我们知道
1 |
path('new-cbv/', MyView.as_view(), name='new_cbv') |
看起来像
1 |
path('new-cbv/', view, name='new_cbv') |
一旦对MyView.as_view()进行了评估,则该外观与使用FBV时的外观相同。 这意味着view()将接受与Django调用时FBV相同的参数。 我们不会介绍Django如何调用视图以及如何执行视图,但是您可以在Django文档中了解更多信息。
在详细介绍view()之前,还有MyView.as_view()的其余部分。 首先,将view.view_class和view.view_initkwargs的视图上的类属性分别设置为view和initkwargs的类。
最后,对update_wrapper()进行了两次调用。 这将从类和dispatch()方法复制元数据进行查看。
完成.as_view()之后,我们现在可以查看view()函数及其功能。
使用view()创建类实例
调用view()时究竟会做什么? 简而言之,它启动了执行视图逻辑并最终返回响应的事件链。 让我们看看它是如何做到的。
1 2 3 4 5 6 7 8 9 |
def view(request, *args, **kwargs): self = cls(**initkwargs) self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs) |
view()具有三个参数:request,* args和** kwargs。 这些由Django在调用带有URL模式的* args和** kwargs的视图时提供。 重要的是不要将** kwargs与我们之前看到的initkwargs混淆。 ** initkwargs来自对as_view()的调用,而** kwargs来自URL中匹配的模式。
view()要做的第一件事是通过传递类** initkwargs并将该实例分配给self来创建该类的实例。 如果我们回到View类的开头,可以看到__init __()方法中设置了类属性,其中** initkwargs在本地称为** kwargs。
1 2 3 4 5 6 7 8 9 |
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 kwargs.items(): setattr(self, key, value) |
创建类实例后,将调用setup()方法,该方法采用传递给view()的相同三个参数。 它将参数保存到类实例,这使它们可用于视图的所有后续方法。
1 2 3 4 5 6 7 |
def setup(self, request, *args, **kwargs): """Initialize attributes shared by all view methods.""" if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs |
它还检查self是否具有get属性和head属性。 如果它有get但没有head,它将创建head并为其分配get。
调用setup()之后,在view()函数中还要进行一项检查,以确保self具有request属性。
1 2 3 4 5 |
if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) |
如果没有请求属性,将显示的错误消息很好地说明了为什么需要进行此检查的原因-可能会覆盖setup()方法,从而可能不会在实例上创建请求属性。 我们将在以后的文章中保留重写类方法的“为什么”和“如何”,但是在创建自己的CBV或修改Django内置的通用CBV时,这很常见。
完成此检查后,view()最终通过调用dispatch()方法返回。
使用dispatch()路由视图逻辑
dispatch()传递了与view()相同的三个参数,并且确实执行了其名称所暗示的功能-它根据请求类型将视图分派到正确的逻辑分支。
1 2 3 4 5 6 7 8 9 |
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) |
它采用请求的HTTP方法,检查以确保它在self.http_method_names中存储的允许方法列表中,然后从self中获取相同名称的对应方法,并将其分配给处理程序。例如,如果发出GET请求,则将为处理程序分配该类的get()方法。或者,如果发出POST请求,则处理程序将是该类的post()方法。如果请求方法不在self.http_method_names中,或者self没有相应的方法,则为处理程序分配http_method_not_allowed方法,该方法将返回HttpResponseNotAllowed。 dispatch()通过调用带有请求,* args和** kwargs参数的分配给处理程序的任何方法来返回。
如果回头查看View类的完整代码,我们会注意到没有get()或post()方法,或者大多数其他HTTP方法的方法。它仅包含方法options()来处理HTTP OPTIONS方法。
那么当我们需要处理GET请求时会发生什么呢?
至此,我们已经达到了View类的极限。视图并不意味着是独立的CBV。它用作其他CBV继承的基类。从View继承的类将定义get(),post()和其他处理请求所需的方法。如果再次查看RedirectView,我们将看到它如何为所有HTTP方法名称定义方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# Inside RedirectView def get(self, request, *args, **kwargs): url = self.get_redirect_url(*args, **kwargs) if url: if self.permanent: return HttpResponsePermanentRedirect(url) else: return HttpResponseRedirect(url) else: logger.warning( 'Gone: %s', request.path, extra={'status_code': 410, 'request': request} ) return HttpResponseGone() def head(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def options(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) |
您创建的其他常规CBV或自定义CBV需要具有定义的方法来处理您希望向视图请求的HTTP方法。
概要
视图有很多事情要做,甚至还不是完整的视图,所以让我们回顾一下我们已经讲过的内容。
View类设置并启动正确处理CBV逻辑所需的事件链
在URLconf中的视图上调用as_view()方法将返回Django将用于处理请求的view()函数。
当Django调用view()时,将创建该类的实例,并调用dispatch()方法
dispatch()路由到基于请求的HTTP方法的适当方法
视图不是完整的视图,而是其他CBV继承的基础
在以后的文章中,我们将详细介绍使用基于类的视图,如何创建自己的视图以及Django内置通用CBV的功能。
原文:https://www.brennantymrak.com/articles/comprehending-class-based-views-view-base-class.html