DjangoのView呼び出しの仕組み ~getメソッドはどう呼ばれるのか

最近仕事でDjangoを使う機会が多くなった。
プログラミングを勉強し始めた頃にDjango Girls Japanのチュートリアルをやったことがあるからまあ大丈夫だろうと思っていたが、すぐにそれだけでは全く歯が立たないと気づいた。業務でDjangoを扱う際は、デフォルトの内部メソッドをオーバーライドしたり、デバッグの際に内部メソッドがどのように呼ばれているかを手がかりにしたりすることが多く、内部の処理を理解していないと実装の方針を立てられないのだ。
そこで今回は、Djangoアーキテクチャのうち、Viewの部分の処理内容を追ってみる。全ての汎用Viewの基本になる、Viewクラスのgetメソッドが呼ばれるまでを見てみたい。
今回は以下のサンプルコードで考える。Djangoのバージョンは2.1とする。

# urls.py
from django.urls import path
from myapp.views import IndexView

urlpatterns = [
    path('', IndexView.as_view(), name='index'),
]
# views.py
from django.shortcuts import render
from django.views.generic import View

class IndexView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'myapp/index.html')

ViewクラスはDjangoのviews/generic/base.pyの中に定義されている。長くなるので全部は掲載しないが、ここの実装を読んでいくと流れを追える。

まず、リクエストに応じて、urls.pyに書いたurlpatternsの中でマッチングが起こり、該当するURLのViewクラスが特定される。このプロセスをオーバーライドすることは少ないので省略する。

URLでViewクラスをimportした時点で__init__が呼ばれる。が、ここはキーワードを属性に入れているだけなのであまり重要ではない。

次に、クラスベースでViewを書いている場合は、明示的にas_view()を呼び出す必要がある。

# base.py
 
    @classonlymethod
    def as_view(cls, **initkwargs):
        """Main entry point for a request-response process."""
        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))

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)
        view.view_class = cls
        view.view_initkwargs = initkwargs

        # 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

これがやっていることは、クロージャであるview(request, *args, **kwargs)関数を返すこと。
ちなみに、実装を書くときによく使うself.requestはこの段階で定義されている。

このview(request, *args, **kwargs)関数は、self.dispatch(self.request, *args, **kwargs)メソッドを呼ぶのだが、この実装が面白い。

# base.py

    http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
    
    #(中略)
    
    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) 

あらかじめ定義してあるhttp_method_namesに、リクエストメソッドと同じものがあれば、その名前のメソッドを呼ぶようになっている。
例えばgetリクエストなら、handlerにgetが入り、get(request, *args, **kwargs)メソッドが呼ばれる。postリクエストならpost(request, *args, **kwargs)となる。
そしてこれらのメソッドは、Viewクラス内に定義はない。views.pyの中に自分で定義する必要があるわけだ。

# views.py
from django.shortcuts import render
from django.views.generic import View

class IndexView(View):
    def get(self, request, *args, **kwargs):
        return render(request, 'myapp/index.html')

getメソッドにsuper()などの記述が不要なのは、もともと定義されていないためである。
こうしてviews.pyのgetメソッドが呼ばれ、templateが返される。

これ以外にも、場合によってはその他のメソッドが呼ばれることがあるが、この流れを抑えておくと、他の汎用ビューの仕組みも理解しやすくなる。