22 Django 的類檢視
前面第9節中我們簡單介紹了 Django FBV 和 CBV,分別表示以函式形式定義的檢視和以類形式定義的檢視。函式檢視便於理解,但是如果一個檢視函式對應的 URL 路徑支援多種不同的 HTTP 請求方式時,如 GET, POST, PUT 等,需要在一個函式中寫不同的業務邏輯,這樣導致寫出的函式檢視可讀性不好。此外,函式檢視的複用性不高,大量使用函式檢視,導致的一個結果就是大量重複邏輯和程式碼,嚴重影響專案質量。而 Django 提供的 CBV 正是要解決這個問題而出現的,這也是官方強烈推薦使用的方式。
1. Django 類檢視使用介紹
1.1 CBV 的基本使用
前面我們已經介紹了 CBV 的基本使用方法,其基本流程如下:
定義檢視類 (TestView)
該類繼承檢視基類 View,然後實現對應 HTTP 請求的方法。Django 在 View 類的基礎上又封裝了許多檢視類,如專門返回模板的 TemplateView 檢視類、用於顯示列表資料的 ListView 檢視類等等。這些封裝的是圖能夠進一步減少大家的重複程式碼,後面我會詳細介紹這些封裝的檢視類的使用以及其原始碼實現。
# 程式碼路徑 hello_app/views.py
# ...
class TestView(View):
def get(self, request, *args, **kwargs):
return HttpResponse( 'hello, get\n')
def post(self, request, *args, **kwargs):
return HttpResponse('hello, post\n')
def put(self, request, *args, **kwargs):
return HttpResponse('hello, put\n')
def delete(self, request, *args, **kwargs):
return HttpResponse('hello, delete\n')
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(TestView, self).dispatch(request, *args, **kwargs)
配置 URLConf,如下:
# 程式碼路徑 hello_app/urls.py
# ...
urlpatterns = [
path('test-cbv/', views.TestView.as_view(), name="test-cbv")
]
注意:不是直接寫檢視類,而是要呼叫檢視類的 as_view() 方法,這個 as_view() 方法返回的也是一個函式。
啟動 Django 工程,測試:
# 啟動django服務
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 15, 2020 - 07:08:32
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C
# 開啟另一個xshell視窗,傳送如下請求
[root@server ~]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/
hello, get
[root@server ~]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/
hello, post
[root@server ~]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/
hello, put
[root@server ~]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/
hello, delete
1.2 Django 中使用 Mixin
首先需要了解一下 Mixin 的概念,這裡有一篇介紹 Python 中 Mixin 的文章:<<多重繼承>> ,可以認真看下,加深對 Mixin 的理解。在我的理解中,Mixin 其實就是單獨的一塊功能類。假設 Django 中提供了 A、B、C 三個檢視類,又有 X、Y、Z三個 Mixin 類。如果我們想要檢視 A,同時需要額外的 X、Y功能,那麼使用 Python 中的多重繼承即可達到目的:
class NewView(A, X, Y):
"""
定義新的檢視
"""
pass
我們來看看 Django 的官方文件是如何引出 Mixin 的:
Django’s built-in class-based views provide a lot of functionality, but some of it you may want to use separately. For instance, you may want to write a view that renders a template to make the HTTP response, but you can’t use TemplateView;perhaps you need to render a template only on
POST
, withGET
doing something else entirely. While you could use TemplateResponse directly, this will likely result in duplicate code.For this reason, Django also provides a number of mixins that provide more discrete functionality. Template rendering, for instance, is encapsulated in the TemplateResponseMixin.
翻譯過來就是: Django 內建的類檢視提供了許多功能,但是我們可能只需要其中的一部分功能。例如我想寫一個檢視,該檢視使用由模板檔案渲染後的 HTML 來響應客戶端的 HTTP 請求,但是我們又不能使用 TemplateView 來實現,因為我只想在 POST 請求上使用這個模板渲染的功能,而在 GET 請求時做其他事情。當然,可以直接使用 TemplateResponse 來完成,這樣就會導致程式碼重複。基於這個原因, Django 內部提供了許多離散功能的 mixins。
可以看到,這裡的 mixins 就是一些單獨功能的類,配合檢視類一起使用,用於組合出各種功能的檢視。接下來,我們結合前面的 Member 表來使用下 mixin 功能。具體的步驟如下:
改造原來的檢視類-TestView。我們給原來的檢視類多繼承一個 mixin,用於實現單個物件查詢查詢功能;
from django.shortcuts import render
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin
from .models import Member
# Create your views here.
class TestView(SingleObjectMixin, View):
model = Member
def get(self, request, *args, **kwargs):
return HttpResponse('hello, get\n')
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return HttpResponse('hello, {}\n'.format(self.object.name))
def put(self, request, *args, **kwargs):
return HttpResponse('hello, put\n')
def delete(self, request, *args, **kwargs):
return HttpResponse('hello, delete\n')
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(TestView, self).dispatch(request, *args, **kwargs)
修改 URLConf 配置,傳遞一個動態引數,用於查詢表中記錄:
urlpatterns = [
path('test-cbv/<int:pk>/', views.TestView.as_view(), name="test-cbv")
]
啟動伺服器,然後進行測試:
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/2/
hello, 會員2
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/4/
hello, spyinx-0
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/9/
hello, spyinx-5
[root@server first_django_app]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/9/
hello, get
[root@server first_django_app]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/9/
hello, put
[root@server first_django_app]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/9/
hello, delete
可以看到在 POST 請求中,我們通過傳遞主鍵值,就能返回 Member 表中對應記錄中的 name 欄位值,這一功能正是由SingleObjectMixin
中的 get_object()
方法提供的。通過繼承這個查詢功能,我們就不用再使用 ORM 模型進行查找了,這簡化了我們的程式碼。當然,這隻能滿足一小部分的場景,對於更多複雜的場景,我們還是需要實現自己的邏輯,我們也可以把複雜的功能拆成各種 mixin,然後相關組合繼承,這樣可以很好的複用程式碼,這是一種良好的編碼方式。
2. 深入理解 Django 類檢視
這裡在介紹完類檢視的基本使用後,我們來深入學習下 Django 的原始碼,看看 Django 是如何將對應的 HTTP 請求對映到對應的函式上。這裡我們使用的是 Django 2.2.10 的原始碼進行說明。我們使用 VSCode 開啟 Django 原始碼,定位到 django/views/generic
目錄下,這裡是和檢視相關的原始碼。
首先看 __init__.py
檔案,內容非常少,主要是將該目錄下的常用檢視類匯入到這裡,簡化開發者匯入這些常用的類。其中最重要的當屬 base.py
檔案中定義的 view
類,它是其他所有檢視類的基類。
# base.py中常用的三個view類
from django.views.generic.base import RedirectView, TemplateView, View
# dates.py中定義了許多和時間相關的檢視類
from django.views.generic.dates import (
ArchiveIndexView, DateDetailView, DayArchiveView, MonthArchiveView,
TodayArchiveView, WeekArchiveView, YearArchiveView,
)
# 匯入DetailView類
from django.views.generic.detail import DetailView
# 匯入增刪改相關的檢視類
from django.views.generic.edit import (
CreateView, DeleteView, FormView, UpdateView,
)
# 匯入list.py中定義的顯示列表的檢視類
from django.views.generic.list import ListView
__all__ = [
'View', 'TemplateView', 'RedirectView', 'ArchiveIndexView',
'YearArchiveView', 'MonthArchiveView', 'WeekArchiveView', 'DayArchiveView',
'TodayArchiveView', 'DateDetailView', 'DetailView', 'FormView',
'CreateView', 'UpdateView', 'DeleteView', 'ListView', 'GenericViewError',
]
# 定義一個通用的檢視異常類
class GenericViewError(Exception):
"""A problem in a generic view."""
pass
接下來,我們檢視 base.py
檔案,重點分析模組中定義的 View 類:
# 原始碼路徑 django/views/generic/base.py
# 忽略匯入
# ...
class View:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
def __init__(self, **kwargs):
# 忽略
# ...
@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("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (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)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
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 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())
# 忽略其他函式
# ...
# ...
我們來仔細分析 view 類中的這部分程式碼。view 類首先定義了一個屬性 http_method_names
,表示其支援的 HTTP 請求方法。接下來最重要的是 as_view()
方法和 dispatch()
方法。在上面使用檢視類的示例中,我們定義的 URLConf 如下:
# first_django_app/hello_app/urls.py
from . import views
urlpatterns = [
# 類檢視
url(r'test-cbv/', views.TestView.as_view(), name='test-cbv'),
]
這裡結合原始碼可以看到,views.TestView.as_view()
返回的結果同樣是一個函式:view(),它的定義和前面的檢視函式一樣。as_view()
函式可以接收一些引數,函式呼叫會先對接收的引數進行檢查:
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (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))
上面的程式碼會對 as_view()
函式傳遞的引數做兩方面檢查:
首先確保傳入的引數不能有 get、post 這樣的 key 值,否則會覆蓋 view 類中的對應方法,這樣對應的請求就無法正確找到函式進行處理。覆蓋的程式碼邏輯如下:
class View:
# ...
def __init__(self, **kwargs):
# 這裡會將所有的傳入的引數通過setattr()方法給屬性類賦值
for key, value in kwargs.items():
setattr(self, key, value)
# ...
@classonlymethod
def as_view(cls, **initkwargs):
# ...
def view(request, *args, **kwargs):
# 呼叫檢視函式時,會將這些引數傳給View類來例項化
self = cls(**initkwargs)
# ...
# ...
# ...
此外,不可以傳遞類中不存在的屬性值。假設我們將上面的 URLConf 進行略微修改,如下:
from . import views
urlpatterns = [
# 類檢視
url(r'test-cbv/', views.TestView.as_view(no_key='hello'), name='test-cbv'),
]
啟動後,可以發現 Django 報錯如下,這正是由本處程式碼丟擲的異常。
接下來看下 update_wrapper() 方法,這個只是 python 內建模組中的一個方法,只是比較少用,所以會讓很多人感到陌生。先看它的作用:
update_wrapper() 這個函式的主要功能是負責複製原函式的一些屬性,如 moudle、name、doc 等。如果不加 update_wrapper(), 那麼被裝飾器修飾的函式就會丟失其上面的一些屬性資訊。
具體看一個測試程式碼示例:
from functools import update_wrapper
def test_wrapper(f):
def wrapper_function(*args, **kwargs):
"""裝飾函式,不保留原資訊"""
return f(*args, **kwargs)
return wrapper_function
def test_update_wrapper(f):
def wrapper_function(*args, **kwargs):
"""裝飾函式,使用update_wrapper()方法保留原資訊"""
return f(*args, **kwargs)
update_wrapper(wrapper_function, f)
return wrapper_function
@test_wrapper
def test_wrapped():
"""被裝飾的函式"""
pass
@test_update_wrapper
def test_update_wrapped():
"""被裝飾的函式,使用了update_wrapper()方法"""
pass
print('不使用update_wrapper()方法:')
print(test_wrapped.__doc__)
print(test_wrapped.__name__)
print()
print('使用update_wrapper()方法:')
print(test_update_wrapped.__doc__)
print(test_update_wrapped.__name__)
執行結果如下:
不使用update_wrapper()方法:
裝飾函式,不保留原資訊
wrapper_function
使用update_wrapper()方法:
被裝飾的函式,使用了update_wrapper()方法
test_update_wrapped
可以看到,不使用 update_wrapper() 方法的話,函式在使用裝飾器後,它的一些基本屬性比如 __name__
等都是正真執行函式(比如上面的 wrapper_function() 函式)的屬性。不過這個函式在分析檢視函式的處理流程上並不重要。接下來看 as_view 中定義的 view() 方法,它是真正執行 HTTP 請求的檢視函式:
def view(request, *args, **kwargs):
self = cls(**initkwargs)
# 如果有get方法而沒有head方法,對於head請求則直接使用get()方法進行處理
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
# 將Django對應傳過來的請求例項以及相應引數賦給例項屬性
self.setup(request, *args, **kwargs)
# 如果沒有request屬性,表明可能重寫了setup()方法,而且setup()裡面忘記了呼叫super()
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
# 呼叫dispatch()方法
return self.dispatch(request, *args, **kwargs)
view() 方法裡面會呼叫 setup()
方法將 Django 給檢視函式傳遞的引數賦給例項變數,然後會呼叫 dispatch()
方法去處理請求。兩個函式的程式碼如下:
def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all view methods."""
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)
這裡最核心的就是這個 dispatch()
方法了。首先該方法通過 request.method.lower()
這個可以拿到 http 的請求方式,比如 get、post、put 等,然後判斷是不是在預先定義好的請求方式的列表中。如果滿足,那麼最核心的程式碼來了:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
假設客戶端發的是 get 請求,那麼 request.method.lower()
就是 “get” ,接下來執行上面的程式碼,就會得到我們定義的檢視類中定義的 get 函式,最後返回的是這個函式的處理結果。這就是為啥 get 請求能對應到檢視函式中get() 方法的原因。其他的請求也是類似的,如果是不支援的請求,則會執行 http_method_not_allowed()
方法。
return handler(request, *args, **kwargs)
如果對這部分程式碼的執行流程還有疑問的,我們可以在 Django 的原始碼中新增幾個 print() 函式,然後通過實際請求來看看執行過程:
[root@server first_django_app]# cat ~/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/views/generic/base.py
class View:
...
@classonlymethod
def as_view(cls, **initkwargs):
...
def view(request, *args, **kwargs):
print('呼叫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.
print('呼叫dispatch()方法處理http請求,請求方式:{}'.format(request.method.lower()))
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
print('得到的handler:{}'.format(handler))
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
接下來我們還是使用前面定義的檢視類 TestView 來進行操作,操作過程以及實驗結果如下:
# 一個視窗啟動 django 工程
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 15, 2020 - 04:30:04
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C.
# 另一個視窗傳送http請求
[root@server django-manual]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/
hello, get
[root@server django-manual]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/
hello, post
[root@server django-manual]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/
hello, put
[root@server django-manual]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/
hello, delete
3. 小結
本小節中,我們簡單介紹了檢視類的使用以及一些高階用法。接下來我們分析了 Django 原始碼中的 View 類以及 Django 是如何將請求對映到對應的函式上執行,這部分程式碼是比較簡單易懂的。只有慢慢深入瞭解 Django 的原始碼,瞭解整個 Django 框架背後為我們做的事情,才能從入門到真正掌握 Django。