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

# 前回のお話

[View編① 基本](https://qiita.com/tsuperis/items/0966721d6ee700770e40)　

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

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

# 今回のお話

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

# 環境

前回と同じです。

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

```shell
$ 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 で動作確認できます

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

```python: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のメソッドで実装」

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

```python
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)
```


### 多重継承地獄

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

```python
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__`という特殊メソッドで確認することも可能です。(重要)**

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

なのでこの場合

1. `A`に`print`が定義されているか→ない
2. `Base001`に`print`が定義されているか→ない
3. `Base002`に`print`が定義されているか→あった、利用しよう

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

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

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

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

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

[Mixin - Wikipedia](https://ja.wikipedia.org/wiki/Mixin)

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

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

```python
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](https://www.atmarkit.co.jp/ait/articles/1909/03/news021.html)
[ミックスインってなに？Pythonのコードで見るミックスインのやり方使い方](https://naruport.com/blog/2019/7/15/python-mixin/)
[ミックスイン | Python Language Tutorial](https://pythonpedia.com/ja/tutorial/4359/------)

# あらためて読んでみる

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

```python:
>>> 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` `ProcessFormView`（`View`は[前回](https://qiita.com/tsuperis/items/0966721d6ee700770e40)読みましたね）を意識しながら読んでみることにします

## `FormView`

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

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

## `BaseFormView`

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

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

## `ProcessFormView`

```python:(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 --- 活動中のオブジェクトの情報を取得する &#8212; Python 3.8.6 ドキュメント](https://docs.python.org/ja/3.8/library/inspect.html)

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

```python: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`

```python:(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`

```python:(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)

```

```python:(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`が使われていますが、ここで設定されたものです。

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

### (C) `get_form`

```python:(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`

```python:(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の[メッセージフレームワーク](https://docs.djangoproject.com/ja/2.2/ref/contrib/messages/)を介したメッセージ送付を行っているということです。

このフレームワークは

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

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

### (E) `form_invalid`

```python:(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`の定義をもう一度見てみます

```python
    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`メソッドです
少し時間をとって何が違うのか見てみると良いかもしれません

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

ポイントだけ抽出すると

```python
    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の触りを読んでみようかなと思っています（リクエストあればそちらをやるかも）