本文为 Django 学习总结,讲解视图 CBV。欢迎交流
FBV 与 CBV
我们以前写的视图都是基于 FBV(Function Based View),即将视图写成函数的方式。例如下面这个添加班级的例子:
def add_class(request):
if request.method == "POST":
class_name = request.POST.get("class_name")
models.Classes.objects.create(name=class_name)
return redirect("/class_list/")
return render(request, "add_class.html")
还有一种方法称为 CBV(Class Based View),这种方法将视图写成一个类的形式,显得逻辑更加清晰:
class AddClass(View):
def get(self, request):
return render(request, "add_class.html")
def post(self, request):
class_name = request.POST.get("class_name")
models.Classes.objects.create(name=class_name)
return redirect("/class_list/")
get
请求和 post
请求分开写,且缺一不可。在对应的 url 配置也需要做出改动。下面是 FBV 的url 配置:
re_path(r'^add_class/$', views.add_class),
而在 CBV 版本中,需要在 url 中使用 as_view()
方法:
re_path(r'^home/$', views.AddClass.as_view()),
as_view 方法的流程
我们需要研究一下 as_view()
的源码。直接按 Ctrl 并点击 View
进入源码,as_view()
是 View
的一个类方法(@classonlymethod
):
@classonlymethod
def as_view(cls, **initkwargs):
...
def view(request, *args, **kwargs):
self = cls(**initkwargs) # 实例化一个AddClass类对象self
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
# 执行函数时,django会传给request对象,将其赋值给类对象的request
self.request = request
self.args = args
self.kwargs = kwargs
return self.dispatch(request, *args, **kwargs)
view.view_class = cls
view.view_initkwargs = initkwargs
...
return view
该方法将 cls
即当前类 AddClass
作为参数。类中定义了 view
并返回它,因此在 url 配置中返回的是 view
函数,本质上和 FBV 相同。请求到来时会执行 view
函数。
看下面的代码,执行函数时,django 会传给 request
对象,将其赋值给类对象的 request
:
self.request = request
self.args = args
self.kwargs = kwargs
我们在视图中进行打印验证:
class home(View):
def get(self, request):
print(self.request is request) # 验证
return render(request, "AddClass.html")
打印得到结果:
在这里还返回了 self.dispatch
,并传入相应的参数。我们实际拿到的方法是 dispatch
执行得到的结果:
return self.dispatch(request, *args, **kwargs)
在 as_view()
后面定义了 dispatch
方法:
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.
# 将method转换为小写,在http_method_names中查找
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)
if
语句将 method
转换为小写,在 http_method_names
中查找。http_method_names
也在 view
中进行了定义:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
列表中定义了 8 种请求方式。如果我们在自己的类中定义了同名列表,程序会查找我们自定义的列表,这时如果在列表中没有找到相应的请求方式则会报错。
如果请求方式被允许,则执行一个 getattr
反射,传入请求方式 request.method.lower()
赋值给 handler
,这样就可以获取到我们自定义的 get
或 post
等方法。:
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
如果获取不到 get
请求方式的方法,则在 if 中返回 self.http_method_not_allowed
。如果没有定义的请求方式,则会在 else 中返回 self.http_method_not_allowed
。
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 HttpResponseNotAllowed(self._allowed_methods())
最后执行 handler
返回结果。
写一个例子验证一下。我们在视图类中定义一个 dispatch
方法:
class AddClass(View):
def dispatch(self, request, *args, **kwargs):
return HttpResponse("Mike is a good man")
def get(self, request):
...
因为我们是在执行请求方法 get
前执行 dispatch
,因此页面中会打印:
CBV 加装饰器
我们还可以在类中使用 View
中的 dispatch
方法,并在执行前后可以增添内容,实现装饰器:
class AddClass(View):
def dispatch(self, request, *args, **kwargs):
# return HttpResponse("Mike is a good man")
# 执行get之前的操作
ret = super(home, self).dispatch(request, *args, **kwargs)
# 执行get之后的操作
return ret
def get(self, request):
...
现在我们想要计算执行视图的流程需要多少时间,可以使用一个统计时间的装饰器。在执行视图前后时间相减即可。
我们写一个 timer
装饰器:
import time
# 统计时间的装饰器
def timer(func):
def inner(*args, **kwargs): # 放被装饰的函数参数,任意多个
# 被装饰函数前后进行操作
start = time.time() # 获取当前时间
ret = func(*args, **kwargs)
# 被装饰函数前后进行操作
print('执行时间为:{}'.format(time.time()-start))
return ret
return inner
然后要将装饰器加到视图函数上。
对于 FBV 的视图函数,直接加在函数前面即可:
@timer
def add_class(request):
if request.method == "POST":
class_name = request.POST.get("class_name")
models.Classes.objects.create(name=class_name)
return redirect("/class_list/")
return render(request, "add_class.html")
得到执行时间:
给 CBV 加装饰器有 3 种方法。首先要使用方法装饰器 method_decorator
,需要导入模块。
第一种方法,直接使用方法装饰器加在类方法前面,相当于使用了带参数的装饰器:
from django.utils.decorators import method_decorator
class AddClass(View):
def dispatch(self, request, *args, **kwargs):
# return HttpResponse("Mike is a good man")
# 执行get之前的操作
ret = super(home, self).dispatch(request, *args, **kwargs)
# 执行get之后的操作
return ret
@method_decorator(timer)
def get(self, request):
...
def post(self, request):
...
得到执行时间:
而如果提交 post
请求则不会打印执行时间。这种方法只给 get
请求使用了装饰器。当我们需要给很多方法加装饰器时,这种方法就比较麻烦了。
第二种方法可以给类中所有方法加装饰器,可以将装饰器加在该类的 dispatch
方法上:
@method_decorator(timer)
def dispatch(self, request, *args, **kwargs):
# return HttpResponse("Mike is a good man")
# 执行get之前的操作
ret = super(home, self).dispatch(request, *args, **kwargs)
# 执行get之后的操作
return ret
因为在执行 get
和 post
等请求方法前会先执行 dispatch
。
第三种方法是直接加在类上。这时需要指定类中需要装饰的方法:
@method_decorator(timer, name='get')
class AddClass(View):
...
如果将 name
指定为 dispatch
,则相当于加在了 View
的 dispatch
方法上:
@method_decorator(timer, name='dispatch')
如果我们在 CBV 中使用 @timer
,也可以打印出视图函数的执行时间,也就是说两者在功能上是没区别的,区别在于装饰器内部。
首先使用 @timer
装饰函数,在装饰器中打印函数、request
和参数:
def timer(func):
def inner(request, *args, **kwargs): # 放被装饰的函数参数,任意多个
print(func)
print('************************************')
print(request, args, kwargs)
# 被装饰函数前后进行操作
start = time.time() # 获取当前时间
ret = func(request, *args, **kwargs)
# 被装饰函数前后进行操作
print('执行时间为:{}'.format(time.time()-start))
return ret
return inner
打印结果如下:
打印出的 func
对应第一行的 home.get
函数,而 request
对应最后一行的 home
的 object
,即 get
函数参数中的 self
,就是对象本身。而最后打印的参数才是我们需要的 request
对象。
而使用 @method_decorator(timer)
打印结果为:
第一行为 bound method
即绑定的方法,包含了 self
对象。这样就会将正确的 request
参数传到 request
变量中。
因此我们需要记住的是,如果使用了 @method_decorator(timer)
,则第一个参数就能拿到 request
对象,若不使用则 *args
的第二个参数才是 request
对象。@method_decorator(timer)
会对函数进行处理。
Python 装饰器在实现的时候,被装饰后的函数其实已经是另一个函数了,即函数名等函数属性会发生变化,为了不受其影响,functools 包总提供了 wrap
的装饰器来消除这样的副作用,使得函数能够保留自己的名称和 Docstring。
对于下面的例子,如果我们不加 wrap
则会打印装饰器的名称和 Docstring:
from functools import wraps
def timer(func):
# @wrap
def inner(*args, **kwargs):
'''decorator'''
print('call inner function')
return func(*args, **kwargs)
return inner
def test():
"""Docstring"""
print('call test function')
print(test.__name__, test.__doc__)
执行结果为 ('inner', 'decorator')
。
加 @wrap
后(解开上面的注释)的打印结果为 ('test', 'Docstring')
。