前回のお話
初投稿記事でしたが、思ったより閲覧数が伸びていました!
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 で動作確認できます
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)
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'>)
なのでこの場合
-
A
にprint
が定義されているか→ない -
Base001
にprint
が定義されているか→ない -
Base002
にprint
が定義されているか→あった、利用しよう
となるわけです。
このくらいのサイズのクラスであればルールを把握していれば問題ないかもしれません。
モジュールが分割されていたら…
似たような名前の関数が存在したら…
引数がばらばらだったら…
2回、3回...と継承回数が増えたら…
すぐにどのメソッドが優先されるか読み取れるでしょうか?
完璧な解決方法ではないMixin
ここで出てくるのがMixin
とよばれるクラスです。
サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスである。
わかりにくいですが、簡単にいえば「共通関数(メソッド)をまとめたクラス」を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
ProcessFormView
(View
は前回読みましたね)を意識しながら読んでみることにします
FormView
class FormView(TemplateResponseMixin, BaseFormView):
"""A view for displaying a form and rendering a template response."""
get
メソッドもpost
メソッドも何もないですね。次にいきます。
BaseFormView
class BaseFormView(FormMixin, ProcessFormView):
"""A base view for displaying a form."""
また何もないです。次にいきます。
ProcessFormView
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.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
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
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)
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
が使われていますが、ここで設定されたものです。
...snip...
{# フォーム本体 #}
<form action="" method="post">
{{ form }}
{% csrf_token %}
<input type="submit">
...snip...
</form>
(C) get_form
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
でやっていることは
-
get_form_class
でFormクラスを取得(インスタンス化はしない) -
get_form_kwargs
ででFormクラスに与える引数を作る - 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
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
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の触りを読んでみようかなと思っています(リクエストあればそちらをやるかも)