3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Python】ゼロから始めるDjangoソースコードリーディング View編②

Last updated at Posted at 2020-11-23

前回のお話

View編① 基本 

初投稿記事でしたが、思ったより閲覧数が伸びていました!
LGTMもいただけてとても励みになります。ありがとうございます。

前回はViewクラスがどやってHTTPリクエストメソッドとインスタンスメソッドを対応付けているか確認しました
環境や前提知識もこちらに書いてあるので、初見の方はさらっと読んでみてください

今回のお話

FormViewを読んでみます
SPAなWebサービスでなければ利用頻度が高いので、このあたりを理解するとカスタマイズの幅が広がるかもしれません

環境

前回と同じです。

Djangoのソースコードは$ python -c "import django; print(django.__path__)"でインストール場所を確認するか、公式リポジトリから引っ張ってきましょう。

$ python --version
Python 3.8.5
$ python -m django --version
2.2.17

FormViewの動作を確認

サンプルコード: https://github.com/tsuperis/read_django_sample

サーバーを起動したらhttp://localhost:8000/form_sample で動作確認できます

form_sample/views.py
class MyFormView(FormView):
    """FormViewサンプル"""
    form_class = MyForm
    success_url = reverse_lazy('form_sample:index')
    template_name = 'form_sample/form_sample.html'

    def form_valid(self, form):
        # バリデーション成功時にメッセージを表示
        msg = form.cleaned_data['message']
        messages.success(self.request, f'フォーム送信を受け付けました: {msg}')
        return super().form_valid(form)
form_sample/forms.py
class MyForm(forms.Form):
    message = forms.CharField()

    def clean_message(self):
        msg = self.cleaned_data['message']
        if '不正' in msg:
            raise forms.ValidationError('不正な文字列が含まれています')
        return msg

前回より少しごちゃごちゃしていますが、大事なのはこの2クラスです。
仕様としては

  • フォーム送信をPOSTメソッドで受け付ける
  • Messageに「不正」という文字列が入っていればバリデーションエラー
  • バリデーション成功時に「フォーム送信を受け付けました」というメッセージを出力する

だけの簡単なアプリケーションです。

POST時の動的な処理を追ってみましょう。

読んでみる、その前に

多重継承とMixin

他の言語では許可されていないこともありますが、Pythonでは言語仕様として多重継承が許可されています
多重軽傷によって複数の基底クラスを取ることができるので、単一責任の原則に則って

「タスクA関連の処理ははクラスAのメソッドで実装」
「タスクB関連の処理ははクラスBのメソッドで実装」

といったようにクラスごとの責務を明確に分離できて良さそうに見えます。

class BaseReader:
    def read(self, filename):
        with open(filename, 'r') as f:
            return f.read() + '\n=========='

class BaseWriter:
    def write(self, filename, content):
        with open(filename, 'a') as f:
            f.write(content)

多重継承地獄

多重継承は便利ではあるのですが、扱い方が難しく厄介な存在にもなりがちです。
こんな場合を考えてみましょう。

class Base000:
    text = 'python'
    def get_text(self):
        return self.text
    def print(self):
        print(self.get_text() + 'です')

class Base001(Base000):
    text = 'python3'
    def foo(self):
        bar()

class Base002(Base000):
    text = 'base'
    def print(self):
        print('djangoです')
    def baz(self):
        time.sleep(1)

class A(Base001, Base002):
    pass

このときA().print()はなにを出力するでしょうか?

.....
....
...
..
.

答えは「djangoです」です

多重継承で属性名が重複した場合、先に継承されたクラス優先されます。
参照順序は__mro__という特殊メソッドで確認することも可能です。(重要)

>>> A.__mro__
(<class '__main__.A'>, <class '__main__.Base001'>, <class '__main__.Base002'>, <class '__main__.Base000'>, <class 'object'>)

なのでこの場合

  1. Aprintが定義されているか→ない
  2. Base001printが定義されているか→ない
  3. Base002printが定義されているか→あった、利用しよう

となるわけです。
このくらいのサイズのクラスであればルールを把握していれば問題ないかもしれません。

モジュールが分割されていたら…
似たような名前の関数が存在したら…
引数がばらばらだったら…
2回、3回...と継承回数が増えたら…

すぐにどのメソッドが優先されるか読み取れるでしょうか?

完璧な解決方法ではないMixin

ここで出てくるのがMixinとよばれるクラスです。

Mixin - Wikipedia

サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスである。

わかりにくいですが、簡単にいえば「共通関数(メソッド)をまとめたクラス」をMixinといいます。
上の例で言えば次のようにMixinをくくりだすことができます(他のパターンもあると思います)

class PrintMixin:
    def print(self):
        print(self.text + 'です')

class Base001:
    text = 'python3'
    def foo(self):
        bar()

class Base002:
    text = 'django'
    def baz(self):
        time.sleep(1)

class A(PrintMixin, Base001, Base002):
    pass

これだと先程よりはわかりやすくなったと思います。

Base000クラスをPrintMixinに置き換えて、各実装クラス内でself.textを定義すればprintメソッドが動作するように書き換えました。
(PrintMixin.printはPrintMixin単体では動作しません)

このような形で多重継承の恩恵に預かりつつ、なるべく継承による名前解決の複雑さを取り除こうとすることを目的にしています。
ただし、Mixinクラスを利用してもMixinを継承したりそれ自体のサイズが膨れてくるとわかりやすさが損なわれてしまうため完璧な解決方法ではないことに注意してください。

あまりこの話ばかりしていると本筋からそれていってしまうのでこのあたりで切り上げますが、こんな手法があることを知っておいてください。

[Python入門]多重継承とmixin
ミックスインってなに?Pythonのコードで見るミックスインのやり方使い方
ミックスイン | Python Language Tutorial

あらためて読んでみる

あらためてFormViewの処理を追っていきましょう。
まず、継承関係を確認してみましょう。

>>> from django.views.generic import FormView
>>> FormView.__mro__
(<class 'django.views.generic.edit.FormView'>, <class 'django.views.generic.base.TemplateResponseMixin'>, <class 'django.views.generic.edit.BaseFormView'>, <class 'django.views.generic.edit.FormMixin'>, <class 'django.views.generic.base.ContextMixin'>, <class 'django.views.generic.edit.ProcessFormView'>, <class 'django.views.generic.base.View'>, <class 'object'>)

多いですね。全部のクラスを読むのは骨が折れそうです。

先程の話でMixinは

サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスである。

ということがわかっているので、Mixin単体で読んでもわかりにくい気がします
なので、Mixinは一旦無視してFormView BaseFormView ProcessFormViewView前回読みましたね)を意識しながら読んでみることにします

FormView

(djangoインストールパス)/django/views/generic/edit.py
class FormView(TemplateResponseMixin, BaseFormView):
    """A view for displaying a form and rendering a template response."""

getメソッドもpostメソッドも何もないですね。次にいきます。

BaseFormView

(djangoインストールパス)/django/views/generic/edit.py
class BaseFormView(FormMixin, ProcessFormView):
    """A base view for displaying a form."""

また何もないです。次にいきます。

ProcessFormView

(djangoインストールパス)/django/views/generic/edit.py
class ProcessFormView(View):
    """Render a form on GET and processes it on POST."""
    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        return self.render_to_response(self.get_context_data())  # -- (A)(B)

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        form = self.get_form()  # -- (C)
        if form.is_valid():  # is_validはFormクラスのメソッド、バリデーション成功した場合の処理
            return self.form_valid(form)  # -- (D)
        else:  # バリデーションエラー時の処理
            return self.form_invalid(form)  # -- (E)

    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
    # object, note that browsers only support POST for now.
    def put(self, *args, **kwargs):
        return self.post(*args, **kwargs)

知らないメソッドがいくつか出ていてますね。
メソッド名である程度何をしているか判断できそうなので、処理内容に当たりをつけてみます。

(A) render_to_response : レンダリングしてHTTPレスポンスを返す?
(B) get_context_data : ?????
(C) get_form : フォームクラスを取得?
(D) form_valid : バリデーション成功時の処理(何をしているのかはわからない)
(E) form_invalid : バリデーション失敗時の処理(何をしているのかはわからない)

まだ正解はわかりませんがこんなかんじでしょうか?(A,BはGETリクエスト時のメソッドなのでさらっと流します)

(余談)メソッドの定義場所の確認方法

お好みのIDEやエディタにタグジャンプ機能があればそれを使えばいいですが、ツールが使えない場合にもコードを全部追う必要はありません。

inspect --- 活動中のオブジェクトの情報を取得する — Python 3.8.6 ドキュメント

inspectという便利モジュールがあるのでこれを使って、メソッドが定義されているファイルやソースコード、定義行を取得することができます。

inspectモジュール使用例
>>> inspect.getsourcefile(FormView.get_context_data)  # 定義されているファイル名を表示
'/home/tsuperis/.local/share/virtualenvs/read_django-RZSPYucy/lib/python3.8/site-packages/django/views/generic/edit.py'
>>> inspect.getsourrcelines(FormView.get_context_data)  # 定義内容と定義業を表示
(['    def get_context_data(self, **kwargs):\n', '        """Insert the form into the context dict."""\n', "        if 'form' not in kwargs:\n", "            kwargs['form'] = self.get_form()\n", '        return super().get_context_data(**kwargs)\n'], 63)

(A) render_to_response

(djangoインストールパス)/django/views/generic/base.py
class TemplateResponseMixin:
    """A mixin that can be used to render a template."""
    template_name = None
    template_engine = None
    response_class = TemplateResponse
    content_type = None

    def render_to_response(self, context, **response_kwargs):
        """
        Return a response, using the `response_class` for this view, with a
        template rendered with the given context.

        Pass response_kwargs to the constructor of the response class.
        """
        response_kwargs.setdefault('content_type', self.content_type)
        return self.response_class(
            request=self.request,
            template=self.get_template_names(),
            context=context,
            using=self.template_engine,
            **response_kwargs
        )

TemplateResponseをインスタンス化して返しています。テンプレートをレンダリングしているみたいですね。

(B) get_context_data

(djangoインストールパス)/django/views/generic/edit.py
class FormMixin(ContextMixin):
...snip...
    def get_context_data(self, **kwargs):
        """Insert the form into the context dict."""
        if 'form' not in kwargs:
            kwargs['form'] = self.get_form()
        return super().get_context_data(**kwargs)

(djangoインストールパス)/django/views/generic/base.py
class ContextMixin:
    """
    A default context mixin that passes the keyword arguments received by
    get_context_data() as the template context.
    """
    extra_context = None

    def get_context_data(self, **kwargs):
        kwargs.setdefault('view', self)
        if self.extra_context is not None:
            kwargs.update(self.extra_context)
        return kwargs

前回見た可変長キーワード引数が引数になっています。

ContextMixinの方でview=self(MyFormViewインスタンス)、FormMixinのほうでform=self.get_form()を設定していました

テンプレートファイルでformが使われていますが、ここで設定されたものです。

form_sample/templates/form_sample/index.html
...snip...
{# フォーム本体 #}
<form action="" method="post">
    {{ form }}
    {% csrf_token %}
    <input type="submit">
...snip...
</form>

(C) get_form

(djangoインストールパス)/django/views/generic/edit.py
class FormMixin(ContextMixin):
    """Provide a way to show and handle a form in a request."""
    initial = {}
    form_class = None
    success_url = None
    prefix = None

    def get_initial(self):
        """Return the initial data to use for forms on this view."""
        return self.initial.copy()

    def get_prefix(self):
        """Return the prefix to use for forms."""
        return self.prefix

    def get_form_class(self):
        """Return the form class to use."""
        return self.form_class

    def get_form(self, form_class=None):
        """Return an instance of the form to be used in this view."""
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(**self.get_form_kwargs())

    def get_form_kwargs(self):
        """Return the keyword arguments for instantiating the form."""
        kwargs = {
            'initial': self.get_initial(),
            'prefix': self.get_prefix(),
        }

        if self.request.method in ('POST', 'PUT'):  # リクエストメソッドがPOSTやPUTだったら引数を追加する
            kwargs.update({
                'data': self.request.POST,
                'files': self.request.FILES,
            })
        return kwargs

get_formでやっていることは

  1. get_form_classでFormクラスを取得(インスタンス化はしない)
  2. get_form_kwargsででFormクラスに与える引数を作る
  3. 1で取得したFormクラスに2で作成した引数を与えてインスタンス化する

です。ここでのポイントはget_form_kwargsです。

詳細は省きますがFormクラスでバリデーションチェックを実行するには、dataもしくはfilesをインスタンス化の引数として与える必要があります。
逆にFormViewのバリデーションチェックはデフォルトではPOSTリクエストやPUTリクエストのときにしか実行されないと言うこともできます。

一瞬戻ってProcessFormViewを確認してみると、確かにPOSTとPUTのときにしかフォームのバリデーションチェック(form.is_valid())が実行されていないことがわかります。

余談ですが、このメソッドをオーバーライドするとPOST/PUTメソッド以外でもフォームのバリデーションが利用できます。
この辺の仕様を知っていると、FormViewを使って検索ページを作成する場合、get_form_kwargsをオーバーライドすればいいことがわかります。

(D) form_valid

(djangoインストールパス)/django/views/generic/edit.py
class FormMixin(ContextMixin):
...snip...
    success_url = None
...snip...
    def get_success_url(self):
        """Return the URL to redirect to after processing a valid form."""
        if not self.success_url:
            raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
        return str(self.success_url)  # success_url may be lazy

    def form_valid(self, form):
        """If the form is valid, redirect to the supplied URL."""
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form):
        """If the form is invalid, render the invalid form."""
        return self.render_to_response(self.get_context_data(form=form))

コードを見ていただければ解説は不要だと思いますがform_validは指定したURL success_url にリダイレクトするだけのメソッドです。

サンプルコードでもオーバーライドしていますが、これはリダイレクト前にDjangoのメッセージフレームワークを介したメッセージ送付を行っているということです。

このフレームワークは

Cookie とセッションをベースにしたメッセージング

なのでリダイレクト前に設定されたメッセージをリダイレクト後に表示させることができます。

(E) form_invalid

(djangoインストールパス)/django/views/generic/edit.py
class FormMixin(ContextMixin):
...snip...
    def form_invalid(self, form):
        """If the form is invalid, render the invalid form."""
        return self.render_to_response(self.get_context_data(form=form))

こちらも簡単でFormView.getとほとんど同じ内容です

ただしひとつだけget_context_dataの引数にform=formとあります。

右辺のformは言わずもがな、MyFormインスタンスですね
左辺のformは何でしょうか?

get_context_dataの定義をもう一度見てみます

    def get_context_data(self, **kwargs):
        """Insert the form into the context dict."""
        if 'form' not in kwargs:
            kwargs['form'] = self.get_form()
        return super().get_context_data(**kwargs)

"form"引数が与えられなかった場合にget_formメソッドで新規にFormインスタンスを生成しています

form_invalidで設定されたFormインスタンスとget_formメソッドで生成されたFormインスタンスは同じクラスのインスタンスですが別物です

ヒントはpostメソッドとget_form_kwargsメソッドです
少し時間をとって何が違うのか見てみると良いかもしれません

.....
....
...
..
.

ポイントだけ抽出すると

    def get_form_kwargs(self):
...snip...
        if self.request.method in ('POST', 'PUT'):
            kwargs.update({
                'data': self.request.POST,
                'files': self.request.FILES,
            })

    def post(self, request, *args, **kwargs):
...snip...
        form = self.get_form()
        if form.is_valid():

つまりform_invalidに渡されるFormインスタンスは

  • 値を設定済み
  • バリデーション実行済(バリデーションエラー)

となっています
これがバリデーションエラー時に{{ form }}でエラーメッセ−ジが出力される理由です
バリデーションエラーの情報がFormインスタンスに含まれているんです

まとめ

FormViewは継承しているクラスは少し多いですが、やっていることは

  • Formインスタンスの生成方法をリクエストメソッドごとに変える
  • バリデーションチェック成功時と失敗時の処理はそれぞれ別のメソッドで行う
    • 成功時はリダイレクト
    • 失敗時はgetメソッドと(ほぼ)同じ

だけです。メソッド名で処理内容がなんとなくわかるので比較的読みやすいコードになっていると思います。

デバッグ方法として__mro__inspectもご紹介しましたが、このあたりを使うと読まなければいけない箇所はある程度絞り込まれます。
簡単なデバッグなど日常的にもよく使うので覚えておいて損はありません!

次回

Modelの触りを読んでみようかなと思っています(リクエストあればそちらをやるかも)

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?