1. 程式人生 > Django入門教學 >22 Django 的類檢視

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, with GET 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() 這個函式的主要功能是負責複製原函式的一些屬性,如 moudlenamedoc 等。如果不加 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。