Django のクラスベースビューを完全に理解する(v5.1 対応)

投稿日:
Django

昔書いた記事を最新版対応にして載せ直します。

はじめに

Django での開発時によく使うのが、クラスベースビューです。

クラスベースビューは「クラスベース汎用ビュー」または「汎用ビュー」と呼ばれることもありますが、データモデルの CRUD のような基本的な機能を簡単に実装できるように、ビューを再利用できるクラスとして提供したものです。

しかし現実のアプリケーションはデータモデルの CRUD のような基本的な機能だけではなく、例えば「2 つのデータモデルを同時に編集させたい」「一覧を表示しつつ新規入力もさせたい」のような、単なる CRUD ではない機能が要求されることもあります。そのような要求が来るたびに開発者は「これはどのクラスベースビューを使えばいいか」を考える必要があります。

このときに間違った判断をすると、後から見て何をやっているのか分からないコードになったり、実装はできたものの、コードが冗長になったりします。そのため、どのクラスをベースに作るかを判断するために、クラスベースビューの仕組みを理解しておく必要があります。

そしてクラスベースビューの仕組みを理解するためには、自分はソースコードを読むのが一番だと考えています。しかし、クラスベースビューはクラス構成が複雑なため、前提知識なしにソースコードを読むのは骨が折れます。

そのため、ソースコードを読むための前提知識として、クラスベースビューのクラス構成を解説するのがこの記事の目的です。そのため、各クラスが提供する機能については詳しく解説しません。

Django のクラスベースビューはどれだけあるか

Django のクラスベースビューを調べるときに役立つのが、Django Class-Based-View Inspectorです。このサイトでは、Django で使われているクラスベースビューを用途別にまとめています。また、クラス名をクリックすることで、クラスの詳細、具体的には継承関係、属性、メソッドが表示されます。

このサイトによれば、Django には 24 個のクラスベースビューがあります。しかしよく使うクラスベースビューは限られています。そのためこの記事では、よく使われる次のクラスベースビュー 9 個に限って解説します。

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):

このときに、メソッド解決順序は次のようになります。

  1. TemplateView
  2. TemplateResponseMixin
  3. ContextMixin
  4. 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()])

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, TemplateResponseMixinTemplateView で出てきました。その他の Mixin クラスについては次のような役割です。

そしてこれがややこしいのですが、 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

勘のいい人なら気づくかもしれませんが、 ListViewDetailView と構成がほぼ同じで、クラスが以下のように変わっただけです。

主な違いは次の通りです。

なお、 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 では何もオーバーライドしておらず、 DetailViewDeletionMixin が追加されているだけでした。

4.0以降では BaseDeleteViewFormMixin も継承しています。4.0のリリースノートのGeneric Viewsの記述 では、削除を確認するためのチェックボックスや、成功時のメッセージ提供に使うことを意図しているようです。

DeletionMixin はその名のとおり、削除機能を実装していますが、 BaseDetailView を継承する理由は何でしょうか。それは、GET メソッドでアクセスすると、データモデルの内容が入った確認画面を表示し、削除ボタンを押して POST メソッドでアクセスすると削除される、それが DeleteView の標準の仕様だからです。

おわりに

Django のクラスベースビューについて一通り解説しました。

この記事を読んだあとは、Django 公式サイトにあるドキュメント、Using mixins with class-based viewsを読んでみてください。この公式ドキュメントでは、クラスベースビューや Mixin を使ってどのように必要な機能を実装していくかの指針が書かれています。

逆に言えば、この指針が理解できれば「これはどのクラスベースビューを使えばいいか」を判断できるようになります。そしてこの記事が、この指針の理解の助けになれば幸いです。

次の記事:
マイページ(プライベートサイト)の第三版終わり、あるいは最近学んでいること
前の記事:
「OSCクロニクル」から持続可能なコミュニティ運営を学ぶ