亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

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() 這個函數的主要功能是負責復制原函數的一些屬性,如 moudle、namedoc 等。如果不加 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。