Help us understand the problem. What is going on with this article?

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

View編② FormView

はじめに

効率的にプログラミングを学習する方法のひとつとして「ソースコードを読む」というものがあります。
かの有名なハッカーになろう(原題: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ではビューは関数/クラスの両方で書くことができます。

hoge/views.py
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')
core/urls.py
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'
(djangoインストールパス)/django/views/generic/base.py
    # -- (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型)をループして引数名をチェックしているようです。

エラーの条件として

  1. key(引数名)がClassBasedView.http_method_namesに存在すること
  2. ClassBasedViewクラスの属性にkeyが存在しないこと

1のhttp_method_namesとはなんぞや?
Viewクラスの先頭で定義されています。

(djangoインストールパス)/django/views/generic/base.py
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']

つまり、このループではすべての引数名が

  1. get, post, put, patch, delete, head, options, traceではない
  2. 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と同じ呼び出し方ができるので関数ベースビューとして利用できそうです。

続きを見ていきましょう。
クラスをインスタンス化してからsetupdispatchメソッドを呼び出しています。

インスタンス化

__init__はこんな感じです。
as_viewの名前付き引数をインスタンスの属性として設定していますが、今回as_viewには引数を指定していないので無視します。

(djangoインストールパス)/django/views/generic/base.py
    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はこのタイミングで設定されます。

(djangoインストールパス)/django/views/generic/base.py
    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を呼び出す

はここからきているようです。

(djangoインストールパス)/django/views/generic/base.py
    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関数の処理フローを簡単にまとめておきます。

  1. Viewクラスをインスタンス化
  2. self.request self.kwargs self.argsを設定する
  3. HTTPリクエストメソッド名と同じインスタンスメソッドを呼び出す

(D) update_wrapper

Djangoのコードではないですが、オリジナルのデコレーターなどを作成するときにも重要なので簡単に紹介しておきます。

console
>>> 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になれてからにしようと思います。。。

続きを書きました

View編② FormView

tsuperis
20代後半戦です。Web開発メインですが最近セキュリティサービスの開発もしてます。 普段はPythonとGoでたまにJavaとC++とPHP
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away