Django のクラスベースビューを完全に理解する(v5.1 対応)
昔書いた記事を最新版対応にして載せ直します。
はじめに
Django での開発時によく使うのが、クラスベースビューです。
クラスベースビューは「クラスベース汎用ビュー」または「汎用ビュー」と呼ばれることもありますが、データモデルの CRUD のような基本的な機能を簡単に実装できるように、ビューを再利用できるクラスとして提供したものです。
しかし現実のアプリケーションはデータモデルの CRUD のような基本的な機能だけではなく、例えば「2 つのデータモデルを同時に編集させたい」「一覧を表示しつつ新規入力もさせたい」のような、単なる CRUD ではない機能が要求されることもあります。そのような要求が来るたびに開発者は「これはどのクラスベースビューを使えばいいか」を考える必要があります。
このときに間違った判断をすると、後から見て何をやっているのか分からないコードになったり、実装はできたものの、コードが冗長になったりします。そのため、どのクラスをベースに作るかを判断するために、クラスベースビューの仕組みを理解しておく必要があります。
そしてクラスベースビューの仕組みを理解するためには、自分はソースコードを読むのが一番だと考えています。しかし、クラスベースビューはクラス構成が複雑なため、前提知識なしにソースコードを読むのは骨が折れます。
そのため、ソースコードを読むための前提知識として、クラスベースビューのクラス構成を解説するのがこの記事の目的です。そのため、各クラスが提供する機能については詳しく解説しません。
Django のクラスベースビューはどれだけあるか
Django のクラスベースビューを調べるときに役立つのが、Django Class-Based-View Inspectorです。このサイトでは、Django で使われているクラスベースビューを用途別にまとめています。また、クラス名をクリックすることで、クラスの詳細、具体的には継承関係、属性、メソッドが表示されます。
このサイトによれば、Django には 24 個のクラスベースビューがあります。しかしよく使うクラスベースビューは限られています。そのためこの記事では、よく使われる次のクラスベースビュー 9 個に限って解説します。
- Generic Base
- モジュール: django.views.generic.base
- クラス: View, TemplateView, RedirectView
- Generic Detail
- モジュール: django.views.generic.detail
- クラス: DetailView
- Generic List
- モジュール: django.views.generic.list
- クラス: ListView
- Generic Edit
- モジュール: django.views.generic.edit
- クラス: FormView, CreateView, UpdateView, DeleteView
Django のクラス構成
まず、9 個のクラスベースビューには、親クラスが 16 個あります。クラスベースビューとその親クラスを合わせて 25 個のクラスの継承関係をクラス図にすると次のようになります。
classDiagram
namespace base {
class ContextMixin
class View
class TemplateResponseMixin
class TemplateView
class RedirectView
}
namespace detail {
class SingleObjectMixin
class BaseDetailView
class SingleObjectTemplateResponseMixin
class DetailView
}
namespace list {
class MultipleObjectMixin
class BaseListView
class MultipleObjectTemplateResponseMixin
class ListView
}
namespace edit {
class FormMixin
class ModelFormMixin
class ProcessFormView
class BaseFormView
class FormView
class BaseCreateView
class CreateView
class BaseUpdateView
class UpdateView
class DeletionMixin
class BaseDeleteView
class DeleteView
}
View <|-- TemplateView
ContextMixin <|-- TemplateView
TemplateResponseMixin <|-- TemplateView
SingleObjectTemplateResponseMixin <|-- CreateView
BaseCreateView <|-- CreateView
ModelFormMixin <|-- BaseCreateView
ProcessFormView <|-- BaseCreateView
FormMixin <|-- ModelFormMixin
SingleObjectMixin <|-- ModelFormMixin
ContextMixin <|-- FormMixin
TemplateResponseMixin <|-- FormView
BaseFormView <|-- FormView
SingleObjectTemplateResponseMixin <|-- DetailView
BaseDetailView <|-- DetailView
SingleObjectMixin <|-- BaseDetailView
View <|-- BaseDetailView
MultipleObjectTemplateResponseMixin <|-- ListView
BaseListView <|-- ListView
SingleObjectTemplateResponseMixin <|-- UpdateView
BaseUpdateView <|-- UpdateView
ModelFormMixin <|-- BaseUpdateView
ProcessFormView <|-- BaseUpdateView
FormMixin <|-- BaseFormView
ProcessFormView <|-- BaseFormView
BaseDeleteView <|-- DeleteView
SingleObjectTemplateResponseMixin <|-- DeleteView
MultipleObjectMixin <|-- BaseListView
View <|-- BaseListView
View <|-- ProcessFormView
DeletionMixin <|-- BaseDeleteView
FormMixin <|-- BaseDeleteView
BaseDetailView <|-- BaseDeleteView
ContextMixin <|-- SingleObjectMixin
TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
ContextMixin <|-- MultipleObjectMixin
TemplateResponseMixin <|-- MultipleObjectTemplateResponseMixin
View <|-- RedirectView
見るのも嫌になりますよね。このように複雑な継承関係になる理由は、Python が多重継承を採用しているからです。
多重継承は名前こそシンプルですが、菱形継承問題などの問題を引き起こしやすい機能です。そのため、最近作られた言語では多重継承は採用されません(Python は 1991 年に誕生した、歴史ある言語です)。
Django では多重継承に関する問題を避けるために、クラス構成を工夫することで、実質的に単一継承にしています。具体的には、Django のクラスベースビューで使われるクラスを「View クラス」と「Mixin クラス」の大きく 2 つに分けています。そして、木の幹となる「View クラス」は単一継承とし、木の枝となる「Mixin クラス」を複数付けられるようにしています。
View クラスは木の幹に相当するもので、次の特徴があります。
- django.views.generic.base.View クラスを頂点とした単一継承
- View クラスのみでビューが作れる
- クラス名の最後に「View」が付いている
- 頂点となる django.views.generic.base.View クラスを除いて、get(), post()など、HTTP メソッドに対応したメソッドのみオーバーライドされている
View クラスのみをクラス図にすると次のようになります。
classDiagram
View <|-- TemplateView
BaseCreateView <|-- CreateView
ProcessFormView <|-- BaseCreateView
BaseFormView <|-- FormView
BaseDetailView <|-- DetailView
View <|-- BaseDetailView
BaseListView <|-- ListView
BaseUpdateView <|-- UpdateView
ProcessFormView <|-- BaseUpdateView
ProcessFormView <|-- BaseFormView
BaseDeleteView <|-- DeleteView
View <|-- BaseListView
View <|-- ProcessFormView
BaseDetailView <|-- BaseDeleteView
View <|-- RedirectView
一方で Mixin クラスは木の枝に相当するもので、次の特徴があります。Java の Interface、Ruby の Module と似たようなものです。
- 特定の機能だけを実装した「部品」を提供する
- Mixin クラスだけでビューは作れない
- クラス名の最後に「Mixin」が付いている
Mixin クラスだけをクラス図にすると次のようになります。
classDiagram
class ContextMixin
class TemplateResponseMixin
class SingleObjectMixin
class SingleObjectTemplateResponseMixin
class MultipleObjectMixin
class MultipleObjectTemplateResponseMixin
class FormMixin
class ModelFormMixin
class DeletionMixin
FormMixin <|-- ModelFormMixin
SingleObjectMixin <|-- ModelFormMixin
ContextMixin <|-- FormMixin
ContextMixin <|-- SingleObjectMixin
TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
ContextMixin <|-- MultipleObjectMixin
TemplateResponseMixin <|-- MultipleObjectTemplateResponseMixin
再度まとめると、全ての View クラスは頂点となる django.views.generic.base.View クラスを除いて、ただ 1 個の View クラスと、0 個以上の Mixin クラスを継承しています。複数の View クラスを継承することはありません。
この仕組みが理解できると、Django のクラス構成が理解しやすくなります。
メソッド解決順序
また、クラスを多重継承する場合、同じメソッドが継承元の複数のクラスで定義されることがあります。そのため、どのクラスのメソッドが優先的に呼ばれるのか、その順番を決める必要があります。
そのメソッドが優先的に呼ばれる順番が「メソッド解決順序」です。英語では Method Resolution Order で、MRO と略します。
そして、Python のメソッド解決順序は次の 2 つの条件を満たしています。
- 派生クラスが基底クラスより優先される
- 継承の定義で左側に書いた基底クラスが右側に書いた基底クラスより優先される
例えば TemplateView は次のように定義されています。
class TemplateView(TemplateResponseMixin, ContextMixin, View):
このときに、メソッド解決順序は次のようになります。
Generic Base
まずは基本となるクラスを集めた、Generic Base について解説します。 Generic Base で解説するクラスはモジュール django.views.generic.base で定義されています。
View
まずは全ての View クラスの頂点となる、django.views.generic.base.View クラスについて解説します。
これ以降のクラス図ではインスタンス変数、クラスメソッド、インスタンスメソッドについて記載しますが、メソッドの引数および戻り値は省略します。
classDiagram
class View {
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
request
args
kwargs
view_is_async$
as_view()$
dispatch()
setup()
http_method_not_allowed()
options()
}
大まかな処理の流れは次のようになっています。
- [as_view()]
- [setup()][View.setup()] で request, args, kwargs をインスタンス変数として登録
- [dispatch()][View.dispatch()] を呼び出す
- [dispatch()] メソッドに対応するメソッドを探して呼び出す(例: GET→get()、POST→post())
- ただし次の場合は [http_method_not_allowed()][View.http_method_not_allowed()] を呼び出す
- HTTP メソッドが [http_method_names][View.http_method_names] に未定義
- HTTP メソッドに対応するメソッドが見つからない
- OPTIONS については定義ずみ([options()][View.options()])
- ただし次の場合は [http_method_not_allowed()][View.http_method_not_allowed()] を呼び出す
RedirectView
次に、リダイレクトを行うためのクラスベースビューである、RedirectView を解説します。クラス図は次のようになります。
classDiagram
class RedirectView {
permanent
url
pattern_name
query_string
get_redirect_url()
get()
head()
post()
options()
delete()
put()
patch()
}
View <|-- RedirectView
いろいろメソッドが定義されていますが、 [head()][View.head()] 以降は全て [get()][View.get()] を呼び出しており、[get()][View.get()] メソッドではリダイレクトしています。すなわち、どの HTTP メソッドを使ってアクセスしてもリダイレクトされます。
TemplateView
次に、シンプルなクラスベースビューの 1 つである、 TemplateView を解説します。クラス図は次のようになります。
classDiagram
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class ContextMixin {
extra_context
get_context_data()
}
class TemplateView {
get()
}
TemplateResponseMixin <|-- TemplateView
ContextMixin <|-- TemplateView
View <|-- TemplateView
TemplateView はテンプレート機能に対応したクラスベースビューです。TemplateView は 2 つの機能からなっており、それぞれ継承している 2 つの Mixin クラスで実装されています。
- TemplateResponseMixin: テンプレートファイルを指定する機能
- ContextMixin: テンプレートファイルに渡すコンテキストを指定する機能
そして、 TemplateView の [get()][TemplateView.get()] メソッドは次のように定義されています。コンテキストを取得して、テンプレートに渡すだけのシンプルな処理です。
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
Generic Detail
次に、詳細を表示するクラスを集めた、Generic Detail について解説します。Generic Detail で解説するクラスはモジュール django.views.generic.detail で定義されています。
DetailView
データモデルを表示するためのクラスベースビューである、DetailView を解説します。クラス図は次のようになります。
classDiagram
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class BaseDetailView {
object
get()
}
class ContextMixin {
extra_context
get_context_data()
}
class SingleObjectMixin {
model
queryset
slug_field = 'slug'
context_object_name
slug_url_kwarg = 'slug'
pk_url_kwarg = 'pk'
query_pk_and_slug
get_object()
get_queryset()
get_slug_field()
get_context_object_name()
get_context_data()
}
class SingleObjectTemplateResponseMixin {
template_name_field
template_name_suffix = '_detail'
get_template_names()
}
class DetailView
SingleObjectTemplateResponseMixin <|-- DetailView
BaseDetailView <|-- DetailView
SingleObjectMixin <|-- BaseDetailView
View <|-- BaseDetailView
ContextMixin <|-- SingleObjectMixin
TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
ContextMixin, TemplateResponseMixin は TemplateView で出てきました。その他の Mixin クラスについては次のような役割です。
- SingleObjectMixin: 単一のデータモデルを取得する
- SingleObjectTemplateResponseMixin: 単一のデータモデルに対応したテンプレート名を取得する
そしてこれがややこしいのですが、 SingleObjectTemplateResponseMixin を使うには次のいずれかが必須です。
- self.template_name が定義されていること
- メソッド get_template_names() が値を返すこと
- self.object が定義されていること
しかし、 SingleObjectTemplateResponseMixin では self.object は定義されていません。この self.object はどこから来るのでしょうか。
それは BaseDetailView です。 [get()][BaseDetailView.get()] メソッドは次のように定義されており、 self.object がここで設定されていること、コンテキスト取得時にそのオブジェクトを渡していることが分かります。
なお、 DetailView ではメソッドはオーバーライドされていません。
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
Generic List
次に、一覧表示を行うクラスを集めた、Generic List について解説します。Generic List で解説するクラスはモジュール django.views.generic.list で定義されています。
ListView
データモデルを一覧表示するためのクラスベースビューである、 ListView を解説します。クラス図は次のようになります。
classDiagram
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class BaseListView {
object_list
get()
}
class ContextMixin {
extra_context
get_context_data()
}
class MultipleObjectMixin {
allow_empty
queryset
model
paginate_by
paginate_orphans
context_object_name
paginator_class
page_kwarg = 'page'
ordering
get_queryset()
get_ordering()
paginate_queryset()
get_paginate_by()
get_paginator()
get_paginate_orphans()
get_allow_empty()
get_context_object_name()
get_context_data()
}
class MultipleObjectTemplateResponseMixin {
template_name_suffix = '_list'
get_template_names()
}
class ListView
MultipleObjectTemplateResponseMixin <|-- ListView
BaseListView <|-- ListView
MultipleObjectMixin <|-- BaseListView
View <|-- BaseListView
ContextMixin <|-- MultipleObjectMixin
TemplateResponseMixin <|-- MultipleObjectTemplateResponseMixin
勘のいい人なら気づくかもしれませんが、 ListView は DetailView と構成がほぼ同じで、クラスが以下のように変わっただけです。
- SingleObjectMixin → MultipleObjectMixin
- SingleObjectTemplateResponseMixin → MultipleObjectTemplateResponseMixin
- BaseDetailView → BaseListView
- DetailView → ListView
主な違いは次の通りです。
- MultipleObjectTemplateResponseMixin は self.template_name, get_template_names() あるいは self.object_list の存在を仮定
- ページネーション機能がある
なお、 ListView ではメソッドはオーバーライドされていません。
Generic Edit
最後に、編集を行うクラスを集めた、Generic Edit について解説します。Generic Edit で解説するクラスはモジュール django.views.generic.edit で定義されています。
FormView
フォームを扱うためのクラスベースビューである、 FormView を解説します。最も複雑で、理解が難しいのがこの FormView です。クラス図は次のようになります。
classDiagram
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class ContextMixin {
extra_context
get_context_data()
}
class FormMixin {
initial
form_class
success_url
prefix
get_initial()
get_prefix()
get_form_class()
get_form()
get_form_kwargs()
get_success_url()
form_valid()
form_invalid()
get_context_data()
}
class BaseFormView
class View
class ProcessFormView {
get()
post()
put()
}
class FormView
TemplateResponseMixin <|-- FormView
ContextMixin <|-- FormMixin
FormMixin <|-- BaseFormView
View <|-- ProcessFormView
ProcessFormView <|-- BaseFormView
BaseFormView <|-- FormView
FormMixin はフォーム作成に必要な情報を作成しています。フォームをカスタマイズするときは、この FormMixin で定義されているメソッドをオーバーライドすることが多いでしょう。そして処理の流れは ProcessFormView で定義されています。 BaseFormView と、 FormView ではメソッドはオーバーライドされていません。
CreateView
次に、データモデルを作成するためのクラスベースビューである、 CreateView を解説します。クラス図は次のようになります。
classDiagram
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class SingleObjectTemplateResponseMixin {
template_name_field
template_name_suffix = '_detail'
get_template_names()
}
class ContextMixin {
extra_context
get_context_data()
}
class FormMixin {
initial
form_class
success_url
prefix
get_initial()
get_prefix()
get_form_class()
get_form()
get_form_kwargs()
get_success_url()
form_valid()
form_invalid()
get_context_data()
}
class SingleObjectMixin {
model
queryset
slug_field
context_object_name
slug_url_kwarg
pk_url_kwarg = 'pk'
query_pk_and_slug
get_object()
get_queryset()
get_slug_field()
get_context_object_name()
get_context_data()
}
class ModelFormMixin {
object
form_class
fields
get_form_class()
get_form_kwargs()
get_success_url()
form_valid()
}
class View
class ProcessFormView {
get()
post()
put()
}
class BaseCreateView {
object = None
get()
post()
}
class CreateView {
template_name_suffix = '_form'
}
TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
SingleObjectTemplateResponseMixin <|-- CreateView
ContextMixin <|-- FormMixin
BaseCreateView <|-- CreateView
ModelFormMixin <|-- BaseCreateView
ProcessFormView <|-- BaseCreateView
FormMixin <|-- ModelFormMixin
SingleObjectMixin <|-- ModelFormMixin
View <|-- ProcessFormView
ContextMixin <|-- SingleObjectMixin
継承関係は複雑ですが、新規に出てくる Mixin クラスは ModelFormMixin ただ 1 つです。この Mixin クラスでは、データモデル固有の実装になるように、メソッドをオーバーライドしています。
ややこしいのは、 DetailView では self.object の設定は BaseDetailView が行っていましたが、 CreateView では self.object の設定は ModelFormMixin が行っていることです。
UpdateView
次に、データモデルを更新するためのクラスベースビューである、 UpdateView を解説します。クラス図は次のようになります。
classDiagram
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class SingleObjectTemplateResponseMixin {
template_name_field
template_name_suffix = '_detail'
get_template_names()
}
class ContextMixin {
extra_context
get_context_data()
}
class FormMixin {
initial
form_class
success_url
prefix
get_initial()
get_prefix()
get_form_class()
get_form()
get_form_kwargs()
get_success_url()
form_valid()
form_invalid()
get_context_data()
}
class ModelFormMixin {
object
form_class
fields
get_form_class()
get_form_kwargs()
get_success_url()
form_valid()
}
class SingleObjectMixin {
model
queryset
slug_field
context_object_name
slug_url_kwarg
pk_url_kwarg = 'pk'
query_pk_and_slug
get_object()
get_queryset()
get_slug_field()
get_context_object_name()
get_context_data()
}
class BaseUpdateView {
object
get()
post()
}
class UpdateView {
template_name_suffix = '_form'
}
class ProcessFormView {
get()
post()
put()
}
FormMixin <|-- ModelFormMixin
SingleObjectMixin <|-- ModelFormMixin
ContextMixin <|-- FormMixin
SingleObjectTemplateResponseMixin <|-- UpdateView
BaseUpdateView <|-- UpdateView
ModelFormMixin <|-- BaseUpdateView
ProcessFormView <|-- BaseUpdateView
View <|-- ProcessFormView
ContextMixin <|-- SingleObjectMixin
TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
これは CreateView が理解できれば簡単です。なぜなら、継承関係がほぼ同じだからです。実装もほぼ同じで、 BaseCreateView が次のような実装なのに対し、
def get(self, request, *args, **kwargs):
self.object = None
return super().get(request, *args, **kwargs)
BaseUpdateView が次のような実装になっています。
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
この実装の違いは、 CreateView が初期状態を持たないのに対して、 UpdateView が初期状態を持つ、それだけです。詳しくは ModelFormMixin の実装を確認してください。
DeleteView
最後に、データモデルの削除を行うためのクラスベースビューである、 DeleteView を解説します。クラス図は次のようになります。
classDiagram
class DeleteView {
template_name_suffix = '_confirm_delete'
}
class ContextMixin {
extra_context
get_context_data()
}
class DeletionMixin {
delete()
post()
get_success_url()
}
class FormMixin {
initial
form_class
success_url
prefix
get_initial()
get_prefix()
get_form_class()
get_form()
get_form_kwargs()
get_success_url()
form_valid()
form_invalid()
get_context_data()
}
class SingleObjectMixin {
model
queryset
slug_field
context_object_name
slug_url_kwarg
pk_url_kwarg = 'pk'
query_pk_and_slug
get_object()
get_queryset()
get_slug_field()
get_context_object_name()
get_context_data()
}
class SingleObjectTemplateResponseMixin {
template_name_field
template_name_suffix = '_detail'
get_template_names()
}
class TemplateResponseMixin {
template_name
template_engine
response_class
content_type
render_to_response()
get_template_names()
}
class BaseDetailView {
object
get()
}
class BaseDeleteView
View <|-- BaseDetailView
SingleObjectMixin <|-- BaseDetailView
BaseDeleteView <|-- DeleteView
SingleObjectTemplateResponseMixin <|-- DeleteView
DeletionMixin <|-- BaseDeleteView
FormMixin <|-- BaseDeleteView
BaseDetailView <|-- BaseDeleteView
ContextMixin <|-- FormMixin
ContextMixin <|-- SingleObjectMixin
TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
DeleteView の特徴は、 BaseDetailView を継承していることです。そして、 BaseDetailView の実装は3.2までと、4.0以降では大きく変わっています。
3.2のときには BaseDeleteView では何もオーバーライドしておらず、 DetailView に DeletionMixin が追加されているだけでした。
4.0以降では BaseDeleteView は FormMixin も継承しています。4.0のリリースノートのGeneric Viewsの記述 では、削除を確認するためのチェックボックスや、成功時のメッセージ提供に使うことを意図しているようです。
DeletionMixin はその名のとおり、削除機能を実装していますが、 BaseDetailView を継承する理由は何でしょうか。それは、GET メソッドでアクセスすると、データモデルの内容が入った確認画面を表示し、削除ボタンを押して POST メソッドでアクセスすると削除される、それが DeleteView の標準の仕様だからです。
おわりに
Django のクラスベースビューについて一通り解説しました。
この記事を読んだあとは、Django 公式サイトにあるドキュメント、Using mixins with class-based viewsを読んでみてください。この公式ドキュメントでは、クラスベースビューや Mixin を使ってどのように必要な機能を実装していくかの指針が書かれています。
逆に言えば、この指針が理解できれば「これはどのクラスベースビューを使えばいいか」を判断できるようになります。そしてこの記事が、この指針の理解の助けになれば幸いです。