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

[View編② FormView](https://qiita.com/tsuperis/items/1aa7cd82d76e4ae9f9c3)

# はじめに

効率的にプログラミングを学習する方法のひとつとして「ソースコードを読む」というものがあります。
かの有名な[ハッカーになろう](https://cruel.org/freeware/hacker.html)（原題：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__)"`でインストール場所を確認するか、[公式リポジトリ](https://github.com/django/django/tree/2.2.17)から引っ張ってきましょう。

```shell
$ python --version
Python 3.8.5
$ python -m django --version
2.2.17
```

# 前提知識

- Python3.xがなんとなくわかる
- Djangoの[チュートリアル](https://docs.djangoproject.com/ja/2.2/intro/)をなんとなく理解している

# 読んでみる

## Viewの基本

サンプルコード: https://github.com/tsuperis/read_django_sample

Djangoではビューは関数/クラスの両方で書くことができます。

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


```python: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](https://docs.djangoproject.com/ja/2.2/ref/urls/#path)の第二引数に設定すればViewとして機能するようです。

ではクラスベースビューでは？
urls.pyを見ると一番の大きな違いは`as_view()`であることに気づけると思います。
ここから読んでいきましょう。

## View.as_view()

インタプリタにコマンドを打ち込んで場所を確認するか、タグジャンプ機能を使って直接クラスを参照してみましょう。

```
>>> from django.views import View
>>> View.__module__
'django.views.generic.base'
```

```python:(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`クラスの先頭で定義されています。

```python:(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関数を見比べてみましょう。

```python
def function_based_view(request):
def view(request, *args, **kwargs):
```

`*args`は可変長引数と呼ばれるもので入力は任意なので、view関数の必須の引数は第一引数だけ。
つまり`function_based_view`と同じ呼び出し方ができるので関数ベースビューとして利用できそうです。

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

#### インスタンス化

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

```python:(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`はこのタイミングで設定されます。

```python:(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`を呼び出す

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

```python:(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のコードではないですが、オリジナルのデコレーターなどを作成するときにも重要なので簡単に紹介しておきます。

```python: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__`以外にも[上書きされる属性](https://docs.python.org/ja/3/library/functools.html#functools.update_wrapper)があるのですが、基本的にはメタデータを置き換える用途で使用されます。

今回の場合、viewという新しい関数にクラス本体や`dispatch`メソッドのメタデータを引き継がせるために使用されています。

# まとめ

`as_view`の処理を簡単にまとめると「HTTPリクエストメソッドに対応したインスタンスメソッドを呼び出すview関数を作る」です。

駆け足でソースコードを追ってみましたが、実際に読むときも


- 実際の動作を確認してから読む
- 本筋と関係ない箇所は関数名や中身をさっと見て雰囲気を掴む

ことがポイントかなーと思います。
細かい処理を追ってもいいですが、全体を把握していない状態だと本筋で何をしていたのかわからなくなってしまうので。

# 次回

特に決めてないですが、FormViewかFormあたりを読んでみようと思います。
Modelも読んでみたいですがメタプログラミングが絡んで長くなりそうなので、もう少しQiitaになれてからにしようと思います。。。

続きを書きました

[View編② FormView](https://qiita.com/tsuperis/items/1aa7cd82d76e4ae9f9c3)