クラスベースビュー入門

クラスベースビューはビューを実装するもう一つの手段で、関数の代わりに Python のオブジェクトとしてビューを定義します。クラスベースビューは関数ベースのビューを完全に置き換えるものではありませんが、関数ベースのビューと比較して、以下のような違いと利点があります。

  • 特定の HTTP メソッド (GETPOST など) に関連するコードの集まりを、条件分岐を使ってかき分けるのではなく、それぞれに独立したメソッドを割り当てることができる。
  • ミックスイン (多重継承) などのオブジェクト指向のテクニックを使って、コードを再利用可能なコンポーネントに分解できる。

ジェネリックビュー、クラスベースビュー、クラスベースジェネリックビューの関係と歴史的経緯

まず初めに存在したのは、ビュー関数の規約だけでした。Django は、定義された関数に HttpRequest を渡して、HttpResponse が返ってくることを期待していました。Django が提供する機能はこの範囲まででした。

早い内に、ビューの開発には共通のイディオムやパターンが存在することが認識されるようになりました。こうしたパターンを抽象化し、共通ケースに当てはまるようなビューの開発を楽にするために導入されたのが、関数ベースのジェネリックビューでした。

関数ベースのジェネリックビューの問題点は、単純なケースを十分にカバーしていたものの、設定オプションを超えて拡張したりカスタマイズしたりする方法がなく、多くの実世界のアプリケーションでの有用性が制限されていたことです。

クラスベースのジェネリックビューは関数ベースのジェネリックビューと同様に、ビューの開発を楽にすることを目的に作成されました。しかし、ミックスインを使用するなどの実装方法の工夫により、ツールキットを提供することができ、結果として、関数ベースのジェネリックビューに比べて、より拡張性が高く、柔軟なものにすることができました。

もしあなたが昔、関数ベースのジェネリックビューを使用しようとして、物足りないと思ったなら、クラスベースのジェネリックビューを、関数ベースからクラスベースにしただけのようなものとは考えないでください。むしろ、ジェネリックビューが解決しようとしていた、元々あった問題を解くための新しいアプローチとしてとらえてください。

Django がクラスベースのジェネリックビューを構築するために使用する基底クラスと mixin のツールキットは、最大限の柔軟性を求めて構築されており、デフォルトのメソッド実装や、最も単純なユースケースでは気にすることのない属性の形で多くのフックを持っています。たとえば、form_class のクラスベースの属性に制限する代わりに、実装では get_form メソッドを使用しており、 get_form_class メソッドを呼び出します。これにより、どのフォームを使用するかを指定するために、属性から完全に動的で呼び出し可能なフックまで、いくつかのオプションが用意されています。これらのオプションは、単純な状況では中途半端な複雑さを追加しているように見えますが、これがなければ、より高度なデザインは制限されてしまいます。

クラスベースのビューを使用する

中核となる機能として、クラスベースのビューでは、HTTP リクエストのメソッドに応じてクラスインスタンスの異なるメソッドを呼び出させることができるため、1つのビュー関数の内部で条件分岐を使わずにすみます。

そのため、ビュー関数の場合に HTTP GET をハンドリングするコードが次のようになるとすると、

from django.http import HttpResponse


def my_view(request):
    if request.method == "GET":
        # <view logic>
        return HttpResponse("result")

クラスベースのビューでは以下のようになります。

from django.http import HttpResponse
from django.views import View


class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse("result")

DjangoのURLリゾルバは、クラスではなく、コール可能な関数にリクエストと関連する引数を送信することを期待しているため、クラスベースのビューには、関連するパターンに一致するURLに対するリクエストが到着したときに呼び出すことができる関数を返す as_view() クラスメソッドがあります。この関数はクラスのインスタンスを作成し、setup() を呼び出してその属性を初期化し、次にその dispatch() メソッドを呼び出します。dispatch はリクエストを調べて、それが GETPOST などであるかを判断し、定義されている場合は一致するメソッドにリクエストを中継し、定義されていない場合は HttpResponseNotAllowed を発生させます:

# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path("about/", MyView.as_view()),
]

メソッドが関数ベースのビューが返すもの、つまり何らかの形の HttpResponse と同等のものしか返せないなら意味はありません。これが意味するのは、 http ショートカットTemplateResponse オブジェクトはクラスベースのビューの内部でも使えるということです。

最小限のクラスベースのビューでは、ジョブを実行するのにどんなクラス属性も必要としませんが、クラスベースの設計をする場合にはクラス属性が役に立つことが多いです。クラス属性のカスタマイズと設定を行うには2つの方法があります。

第1の方法は、通常の Python のサブクラス化を行い、サブクラス上で属性やメソッドを上書きするという方法です。たとえば、次のように親クラスが greeting という属性を持っていたとすると、

from django.http import HttpResponse
from django.views import View


class GreetingView(View):
    greeting = "Good Day"

    def get(self, request):
        return HttpResponse(self.greeting)

サブクラスでは次のように属性を上書きできます。

class MorningGreetingView(GreetingView):
    greeting = "Morning to ya"

もう一つの方法は、URLconf 内での as_view() の呼び出し時に、クラス属性をキーワード引数として指定する方法です。

urlpatterns = [
    path("about/", GreetingView.as_view(greeting="G'day")),
]

注釈

定義したクラスはリクエストが発行されるごとにインスタンス化されますが、as_view() エントリーポイントで指定したクラス属性が設定されるのは、URL がインポートされる際の1回だけです。

ミックスイン (mixin) を使用する

ミックスインは、複数の親クラスのメソッドや属性を混合することができる多重継承の形式の1つです。

たとえば、ジェネリッククラスベースのビューには、 TemplateResponseMixin と呼ばれるミックスインがあり、その主な目的はメソッド render_to_response() を定義することです。 View 基底クラスの動作と組み合わせると、適切なマッチングメソッド( View クラス基底クラスで定義されている動作)にリクエストをディスパッチする TemplateView メソッドクラスとなり、 template_name 属性を使用してTemplateResponseオブジェクト(TemplateResponseMixin で定義されている動作)を返す render_to_response() メソッドを持っています。

ミックスインは複数のクラスにまたがってコードを再利用するためのよい方法ですが、それなりの代償も伴います。コードがミックスインの中に散らばると、子クラスを読んだときそのクラスが何をしているのかを正確に知ることが難しくなり、深い継承ツリーを持つクラスをサブクラス化している場合、どのミックスインのどのメソッドをオーバーライドすればいいのかわからなくなります。

また、1つの汎用ビューからしか継承できないことにも注意してください - つまり、親クラスは1つだけ View を継承することができ、残りのクラスは(もしあれば)ミックスインでなければなりません。View を継承している複数のクラスから継承しようとすると - たとえば、リストの先頭にあるフォームを使用して ProcessFormViewListView を組み合わせようとすると - 期待通りには動作しません。

クラスベースのビューでフォームを扱う

フォームを扱う基本の関数ベースのビューは、次のようなコードになります。

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import MyForm


def myview(request):
    if request.method == "POST":
        form = MyForm(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect("/success/")
    else:
        form = MyForm(initial={"key": "value"})

    return render(request, "form_template.html", {"form": form})

同等のクラスベースのビューは、次のようになるでしょう。

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views import View

from .forms import MyForm


class MyFormView(View):
    form_class = MyForm
    initial = {"key": "value"}
    template_name = "form_template.html"

    def get(self, request, *args, **kwargs):
        form = self.form_class(initial=self.initial)
        return render(request, self.template_name, {"form": form})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect("/success/")

        return render(request, self.template_name, {"form": form})

これは最低限のケースですが、クラス属性を上書きするなどの方法でこのビューをカスタマイズする手段が用意されていることがわかります。たとえば、 form_class を URLconf 設定で指定したり、サブクラス化して1つ以上のメソッドを上書きしたりすることもできます。

クラスベースのビューをデコレーションする

クラスベースのビューを拡張する方法は、ミックスインの使用にとどまりません。デコレータも使用できます。クラスベースのビューは関数ではないので、as_view() を使用した場合とサブクラスを作成した場合では、デコレータは違った動作をします。

URLconf でデコレーションする

クラスベースのビューをデコレーションするには、 as_view() メソッドの結果をデコレートします。これが最も簡単にできる場所は、ビューをデプロイする URLconf の中です。

from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView

from .views import VoteView

urlpatterns = [
    path("about/", login_required(TemplateView.as_view(template_name="secret.html"))),
    path("vote/", permission_required("polls.can_vote")(VoteView.as_view())),
]

このアプローチではデコレータはインスタンスごとに適用されます。もしあるビューのすべてのインスタンスをデコレーションしたいばあいは、別のアプローチを取る必要があります。

クラスをデコレーションする

クラスベースのビューのすべてのインスタンスをデコレーションするには、クラスの定義自体をデコレーションする必要があります。そのためには、クラスの dispatch() メソッドにデコレータを付けます。

クラス上のメソッドはスタンドアロンの関数と完全に同じではないため、関数デコレータを単純にそのままメソッドに適用することはできません。適用前にメソッドデコレータに変換する必要があります。method_decorator デコレータを使えば、関数デコレータをメソッドデコレータに変換し、インスタンスのメソッドのデコレーションに使えるようにできます。たとえば、次のように使用します。

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView


class ProtectedView(TemplateView):
    template_name = "secret.html"

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

あるいは、より簡潔に、クラスを代わりにデコレートして、デコレーション対象のメソッド名をキーワード引数 name に渡すという方法もあります。

@method_decorator(login_required, name="dispatch")
class ProtectedView(TemplateView):
    template_name = "secret.html"

共通のデコレータ群が複数の場所で呼ばれる場合には、デコレータのリストまたはタプルを定義して、これを method_decorator() を複数回呼ぶ代わりに使用できます。以下の2つのクラスは同じになります。

decorators = [never_cache, login_required]


@method_decorator(decorators, name="dispatch")
class ProtectedView(TemplateView):
    template_name = "secret.html"


@method_decorator(never_cache, name="dispatch")
@method_decorator(login_required, name="dispatch")
class ProtectedView(TemplateView):
    template_name = "secret.html"

デコレータは、デコレータに渡された順番でリクエストを処理します。上の例では、never_cache()login_required() の前にリクエストを処理します。

この例では、 ProtectedView のすべてのインスタンスがログイン保護されます。これらの例では login_required を使っていますが、 LoginRequiredMixin を使っても同じことができます。

注釈

method_decorator は、クラスのデコレートするメソッドに *args**kwargs を引数として渡します。定義されているメソッドが互換性のある引数のセットを受け取れない場合には TypeError 例外が発生します。

Back to Top