はじめに
効率的にプログラミングを学習する方法のひとつとして「ソースコードを読む」というものがあります。
かの有名なハッカーになろう(原題:How To Become A Hacker)にもこう書かれています。
しかし、本や講習会のコースでダメだとは言っておきましょう。多くの、いやひょっとしてほとんどのハッカーたちは我流で勉強してきたのです。役に立つのは、(a) コードを読むこと、そして (b) コードを書くことです。
しかし、「コードを書く」ことはよくあっても、「コードを読む」ことはあまりできていない人が多いと感じています。(難易度的にも「読む」ほうが難しいことが多いし…)
本記事ではPythonのメジャーなWebフレームワークであるDjangoを読んで、ソースコードを読む雰囲気をお伝えするものです。
これをきっかけにオープンソースのコードを読んでみようという人が一人でも増えれば幸いです!
今回はまずは第一弾としてクラスベースのViewの流れを追っていきます。
Qiita初投稿で書き方があまりわかっていないので引用の仕方など間違いあればコメントで指摘いただけると助かります。
環境
以下の環境で進めていきます。
Djangoは執筆時点(2020年11月)で最新の2.2.17、Pythonのバージョンはこだわりすぎなくても大丈夫ですが今回は3.8.5で進めていきます。
ソースコードを読んでいくときはPyCharmやEclipseなどタグジャンプが使えるお好みのエディタやIDEを使うのがオススメです。
Djangoのソースコードは $ python -c "import django; print(django.__path__)"
でインストール場所を確認するか、公式リポジトリから引っ張ってきましょう。
$ python --version
Python 3.8.5
$ python -m django --version
2.2.17
前提知識
- Python3.xがなんとなくわかる
- Djangoのチュートリアルをなんとなく理解している
読んでみる
Viewの基本
サンプルコード: https://github.com/tsuperis/read_django_sample
Djangoではビューは関数/クラスの両方で書くことができます。
from django.http.response import HttpResponse
from django.views import View
def function_based_view(request):
"""関数ベースビュー"""
return HttpResponse('function_based_view')
class ClassBasedView(View):
"""クラスベースビュー"""
def get(self, request):
return HttpResponse('class_based_view GET')
def post(self, request):
return HttpResponse('class_based_view POST')
from django.urls import path
from hoge import views as hoge_views
urlpatterns = [
path('func/', hoge_views.function_based_view, name='hoge_func'),
path('cls/', hoge_views.ClassBasedView.as_view(), name='hoge_cls'),
]
これは、
-
/func/
にアクセスするとHTTPリクエストメソッドによらず"function_based_view"がレスポンスされる -
/cls/
にGETでリクエストすると"class_based_view GET"がレスポンスされる -
/cls/
にPOSTでリクエストすると"class_based_view POST"がレスポンスされる -
/cls/
にGET/POST以外でリクエストするとHTTPステータス405のMethod Not Allowedがレスポンスされる
という意味になります。
なぜ関数ベースビューとクラスベースビューで違う動きをするのでしょうか。
まずはじめに見た目が簡単な関数ベースビューfunction_based_view
とurls.pyを見ると、どうやら関数をdjango.urls.pathの第二引数に設定すればViewとして機能するようです。
ではクラスベースビューでは?
urls.pyを見ると一番の大きな違いはas_view()
であることに気づけると思います。
ここから読んでいきましょう。
View.as_view()
インタプリタにコマンドを打ち込んで場所を確認するか、タグジャンプ機能を使って直接クラスを参照してみましょう。
>>> from django.views import View
>>> View.__module__
'django.views.generic.base'
# -- (A)
@classonlymethod
def as_view(cls, **initkwargs):
"""Main entry point for a request-response process."""
# -- (B)
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
# -- (C)
def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.setup(request, *args, **kwargs)
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
return self.dispatch(request, *args, **kwargs)
view.view_class = cls
view.view_initkwargs = initkwargs
# -- (D)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view
(A) 引数
classonlymethod
デコレーターは今回無視します。
名前的にclassmethod
(インスタンス化されていないクラスで呼び出し可能なメソッドにつけるデコレータ)みたいなものくらい認識で。
ClassBasedView().as_view()
ではなくClassBasedView.as_view()
と書ける理由はこのあたりにあります。
本筋に戻ると、引数はふたつ
- cls
- **initkwargs
cls
はクラスメソッドの慣習でClassBasedView
が入ります。
**initkwargs
のように**からはじまる引数は、可変長キーワード引数と呼ばれ、任意の名前付き引数をとってdict型として扱われます。
例えばas_view(name='view', number=1)
と呼び出したとすると、initkwargsの中身は{'name': 'view', number=1}
となります。
>>> def kw(hoge, **kwargs):
... print(f'kwargs: {kwargs}')
...
>>> kw('hoge', name='view', number=123)
kwargs: {'name': 'view', 'number': 123}
(B) ループで引数をチェック
難しい処理がないのでサクサク行きます。
initkwargs(dict型)をループして引数名をチェックしているようです。
エラーの条件として
-
key
(引数名)がClassBasedView.http_method_names
に存在すること -
ClassBasedView
クラスの属性にkey
が存在しないこと
1のhttp_method_names
とはなんぞや?
View
クラスの先頭で定義されています。
class View:
"""
Intentionally simple parent class for all views. Only implements
dispatch-by-method and simple sanity checking.
"""
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
つまり、このループではすべての引数名が
- get, post, put, patch, delete, head, options, traceではない
-
as_view
,http_method_names
などクラスの属性名として存在する
をチェックしていることがわかりました。
(C) view関数の作成
ここが今回の肝です。
view関数を定義しています。as_view
の戻り値もみてみると、この関数が返却されているのがわかります。
先程作った関数ベースビューとview関数を見比べてみましょう。
def function_based_view(request):
def view(request, *args, **kwargs):
*args
は可変長引数と呼ばれるもので入力は任意なので、view関数の必須の引数は第一引数だけ。
つまりfunction_based_view
と同じ呼び出し方ができるので関数ベースビューとして利用できそうです。
続きを見ていきましょう。
クラスをインスタンス化してからsetup``dispatch
メソッドを呼び出しています。
インスタンス化
__init__
はこんな感じです。
as_view
の名前付き引数をインスタンスの属性として設定していますが、今回as_view
には引数を指定していないので無視します。
def __init__(self, **kwargs):
"""
Constructor. Called in the URLconf; can contain helpful extra
keyword arguments, and other things.
"""
# Go through keyword arguments, and either save their values to our
# instance, or raise an error.
for key, value in kwargs.items():
setattr(self, key, value)
setupメソッド
ここは簡単なのでコードを載せるだけにします。
クラスベースビューを使っているとよく見るself.request
はこのタイミングで設定されます。
def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all view methods."""
self.request = request
self.args = args
self.kwargs = kwargs
dispatchメソッド
request.method
には読んで字のごとくHTTPリクエストメソッド名が入っています。
getattr
でHTTPリクエストメソッド名と同じインスタンスメソッドを取得しようとしていますね。
つまり
- /cls/にGETでリクエストすると"class_based_view GET"がレスポンスされる
-
self.get
を呼び出す
-
- /cls/にPOSTでリクエストすると"class_based_view POST"がレスポンスされる
-
self.post
を呼び出す
-
- /cls/にGET/POST以外でリクエストするとHTTPステータス405のMethod Not Allowedがレスポンスされる
-
self.http_method_allowed
を呼び出す
-
はここからきているようです。
def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed) # ここで
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
view関数まとめ
view関数の処理フローを簡単にまとめておきます。
- Viewクラスをインスタンス化
-
self.request
self.kwargs
self.args
を設定する - HTTPリクエストメソッド名と同じインスタンスメソッドを呼び出す
(D) update_wrapper
Djangoのコードではないですが、オリジナルのデコレーターなどを作成するときにも重要なので簡単に紹介しておきます。
>>> from functools import update_wrapper
>>> def view():
... """これはview関数です"""
... pass
...
>>> def dispatch():
... """これはdispatch関数です"""
... pass
...
>>> view.__doc__
'これはview関数です'
>>> update_wrapper(view, dispatch)
<function dispatch at 0x7f90328194c0>
>>> view.__doc__
'これはdispatch関数です'
実例はこんな形になるのですが、おわかりでしょうか?
関数を呼び出すとview.__doc__
がdispatch.__doc__
に置き換えられています。
__doc__
以外にも上書きされる属性があるのですが、基本的にはメタデータを置き換える用途で使用されます。
今回の場合、viewという新しい関数にクラス本体やdispatch
メソッドのメタデータを引き継がせるために使用されています。
まとめ
as_view
の処理を簡単にまとめると「HTTPリクエストメソッドに対応したインスタンスメソッドを呼び出すview関数を作る」です。
駆け足でソースコードを追ってみましたが、実際に読むときも
- 実際の動作を確認してから読む
- 本筋と関係ない箇所は関数名や中身をさっと見て雰囲気を掴む
ことがポイントかなーと思います。
細かい処理を追ってもいいですが、全体を把握していない状態だと本筋で何をしていたのかわからなくなってしまうので。
次回
特に決めてないですが、FormViewかFormあたりを読んでみようと思います。
Modelも読んでみたいですがメタプログラミングが絡んで長くなりそうなので、もう少しQiitaになれてからにしようと思います。。。
続きを書きました