Djangoのクラスベースビューを完全に理解する

Djangoのクラスベースビューを完全に理解する

はじめに

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

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

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

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

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

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

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

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

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

Djangoのクラス構成

まず、9個のクラスベースビューには、親クラスが16個あります。クラスベースビューとその親クラスを合わせて25個のクラスの継承関係をクラス図にすると次のようになります。

見るのも嫌になりますよね。このように複雑な継承関係になる理由は、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クラスのみをクラス図にすると次のようになります。

一方でMixinクラスは木の枝に相当するもので、次の特徴があります。JavaのInterface、RubyのModuleと似たようなものです。

  • 特定の機能だけを実装した「部品」を提供する
  • Mixinクラスだけでビューは作れない
  • クラス名の最後に「Mixin」が付いている

Mixinクラスだけをクラス図にすると次のようになります。

再度まとめると、全ての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 クラスについて解説します。

これ以降のクラス図ではインスタンス変数、クラスメソッド、インスタンスメソッドについて記載しますが、メソッドの引数および戻り値は省略します。

大まかな処理の流れは次のようになっています。

  • as_view(): 次の関数を定義して返す
    • setup() で request, args, kwargs をインスタンス変数として登録
    • dispatch() を呼び出す
  • dispatch(): HTTPメソッドに対応するメソッドを探して呼び出す(例: GET→get()、POST→post())

RedirectView

次に、リダイレクトを行うためのクラスベースビューである、 RedirectView を解説します。クラス図は次のようになります。

いろいろメソッドが定義されていますが、 head() 以降は全て get() を呼び出しており、 get() メソッドではリダイレクトしています。すなわち、どのHTTPメソッドを使ってアクセスしてもリダイレクトされます。

TemplateView

次に、シンプルなクラスベースビューの1つである、 TemplateView を解説します。クラス図は次のようになります。

TemplateView はテンプレート機能に対応したクラスベースビューです。TemplateView は2つの機能からなっており、それぞれ継承している2つのMixinクラスで実装されています。

  • TemplateResponseMixin: テンプレートファイルを指定する機能
  • ContextMixin: テンプレートファイルに渡すコンテキストを指定する機能

そして、TemplateViewget() メソッドは次のように定義されています。コンテキストを取得して、テンプレートに渡すだけのシンプルな処理です。

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 を解説します。クラス図は次のようになります。

ContextMixin, TemplateResponseMixinTemplateView で出てきました。その他のMixinクラスについては次のような役割です。

そしてこれがややこしいのですが、 SingleObjectTemplateResponseMixin を使うには次のいずれかが必須です。

  • self.template_name が定義されていること
  • メソッド get_template_names() が値を返すこと
  • self.object が定義されていること

しかし、SingleObjectTemplateResponseMixin では self.object は定義されていません。この self.object はどこから来るのでしょうか。

それは 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 を解説します。クラス図は次のようになります。

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

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

なお、 ListView ではメソッドはオーバーライドされていません。

Generic Edit

最後に、編集を行うクラスを集めた、 Generic Edit について解説します。 Generic Edit で解説するクラスはモジュール django.views.generic.edit で定義されています。

FormView

フォームを扱うためのクラスベースビューである、 FormView を解説します。最も複雑で、理解が難しいのがこの FormView です。クラス図は次のようになります。

FormMixin はフォーム作成に必要な情報を作成しています。フォームをカスタマイズするときは、この FormMixin で定義されているメソッドをオーバーライドすることが多いでしょう。そして処理の流れは ProcessFormView で定義されています。 BaseFormView と、 FormView ではメソッドはオーバーライドされていません。

CreateView

次に、データモデルを作成するためのクラスベースビューである、 CreateView を解説します。クラス図は次のようになります。

継承関係は複雑ですが、新規に出てくるMixinクラスは ModelFormMixin ただ1つです。このMixinクラスでは、データモデル固有の実装になるように、メソッドをオーバーライドしています。

ややこしいのは、 DetailView では self.object の設定は BaseDetailView が行っていましたが、 CreateView では self.object の設定は ModelFormMixin が行っていることです。

UpdateView

次に、データモデルを更新するためのクラスベースビューである、UpdateView を解説します。クラス図は次のようになります。

これは 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 を解説します。クラス図は次のようになります。

DeleteViewの特徴は、 BaseDetailView を継承していることです。実際には BaseDeleteView では何もオーバーライドしていないため、 DetailViewDeletionMixin が追加されている、ほぼそれだけです。

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

おわりに

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

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

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

採用情報

メンバーズエッジで最高のチームで最高のプロダクトを作りませんか?

最高のプロダクトをつくる 最高のチームで働く

在宅でも、地方でも、首都圏でも。多様な働き方で最高のチームをつくり、お客様のプロダクトパートナーを目指します。アジャイル開発を通じ、開発現場の第一線で活躍し続けたいエンジニアを募集しています。