djangoのlogin_requiredデコレーター実装について調べた

## デコレーターとは

関数の前後に他の特定の処理を入れ込む機能です。オープンソースでも使われているとの情報を確認し、実際の実装を見ていましたが、djangoで使われていると記事を見たので、コードを調べて複雑な点をメモ書きとして残しておきます。


## djangoの login_required の例

https://github.com/django/django/blob/main/django/contrib/auth/decorators.py

```python

def login_required(
    function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None
):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = user_passes_test(
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name,
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

```

user_passes_testの返り値は関数なので、actual_decoratorも関数です。
user_passes_test内部で処理された内容を元に実行する関数を決定し、関数を処理します。デコレーターは単に元の呼び出し関数を加工するだけではなく、全く別の関数に差し替えることも可能ということですね。

コードリーディング中に以下の2点がわかりづらかったので確認します。

1. デコレーターでの関数の差し替え
2. wrapsの使い所 
3. 関数型の使い所


## デコレーターによる関数の差し替えの例

以下の部分で、返答する関数オブジェクトが分岐されています。

```python

def user_passes_test(
    test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME
):
    """
    Decorator for views that checks that the user passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the user object and returns True if the user passes.
    """

    def decorator(view_func):
        def _redirect_to_login(request):

            # 省略

                if test_pass:
                    return await view_func(request, *args, **kwargs)
                return _redirect_to_login(request)

        # Attributes used by LoginRequiredMiddleware.
        _view_wrapper.login_url = login_url
        _view_wrapper.redirect_field_name = redirect_field_name

        return wraps(view_func)(_view_wrapper)

```

処理としてはわかりやすいですが、関数オブジェクトで扱っているため少し難易度が高いですね。

:::note info

デコレーター内部で本来の呼び出し元関数を別の関数に差し替えることが可能(コードリーディングは難しくなりそう)

:::

以下のシンプルなコードで考えてみます。

``
デコレーターBefore
changed_methodの内部出力
デコレーターAfter
結果: 100
``

となります。本来は引数の2乗を返答するmy_function関数を呼び出しているつもりですが、実際に返答はsimple_decorator内部で新たに定義したchanged_methodで呼び出し元関数を差し替えているため、返り値100で返されています。ただしこの実装、読み解くのは正直難しかったですね。

```python

def simple_decorator(target_func):
    def _wrapper(*args, **kwargs):
        """_wrapperのdocs"""
        def changed_method():
            return 100
        print("デコレーターBefore")
        # print(target_func.__name__)
        result = changed_method()
        print("デコレーターAfter")

        return result

    return _wrapper

@simple_decorator
def my_function(x):
    """
    my_functionのDocstring
    >>> my_function(3)
    9
    """
    print("my_functionの内部出力")
    return x * x

if __name__ == '__main__':
    res = my_function(3)
    print(f"結果: {res}")

```

## functools.wrapsについて

### wrapsができること

djangoの中で以下の実装が気になりました。

```
        return wraps(view_func)(_view_wrapper)
```

調べると、wrapsという関数があります。

- 関数名を適切に設定
- ドキュメンテーション文字列（docstring）を適切に設定

https://blog.pyq.jp/entry/Python_kaiketsu_201201

直感で理解するのが難しいので、実際にテストしてみます。以下の記事が非常に参考になります。

https://qiita.com/moonwalkerpoday/items/9bd987667a860adf80a2


:::note info

wrapsの処理を行わない場合には関数名を利用したり、docs系の利用を行おうとした場合にエラーが起きる（doctestも実行されなくなってしまう）。デコレーターの中でwrapsを利用して処理しておくことでこれらの利用が可能になる。

:::

処理する方法について調べましたが、記事中の書き方もできるが、djangoの書き方もあるようです。


```python

from functools import wraps

# django内部での実装部分

wraps(target_func)(_wrapper)

# 他のサイトで見つけた処理
@wraps(_wrapper)

```

### 結論

きちんと処理したデコレーターの実装. doctestも通ります。

```python

from functools import wraps
import doctest


def my_decorator(target_func):
    @wraps(target_func)
    def _wrapper(*args, **kwargs):
        """_view_wrapperのdocs"""
        result = target_func(*args, **kwargs)
        return result

    #return wraps(target_func)(_wrapper)
    return _wrapper

@my_decorator
def my_function(x):
    """
    デこられた関数のDocstringだよ
    >>> my_function(3)
    9
    """

    return x * x


if __name__ == '__main__':
    result = my_function(3)
    print(f"result: {result}")
    print(my_function.__name__)
    print(my_function.__doc__)
    doctest.testmod()

```

出力結果。きちんとdoctestも実行されていました。

```
result: 9
my_function

    デこられた関数のDocstringだよ
    >>> my_function(3)
    9
    
Trying:
    my_function(3)
Expecting:
    9
ok
2 items had no tests:
    __main__
    __main__.my_decorator
1 items passed all tests:
   1 tests in __main__.my_function
1 tests in 3 items.
1 passed and 0 failed.
Test passed.

```


もしwrapsをコメントアウトした場合には出力はこうなります。doctestは実行されておらず、関数名が_wrapperになっていますね。

```
result: 9
_wrapper
_view_wrapperのdocs
3 items had no tests:
    __main__
    __main__.my_decorator
    __main__.my_function
0 tests in 3 items.
0 passed and 0 failed.
Test passed.
```


djangoのような書き方でも同じことになります。目的は一緒と思います。

```python

from functools import wraps
import doctest


def my_decorator(target_func):
    #@wraps(target_func)
    def _wrapper(*args, **kwargs):
        """_view_wrapperのdocs"""
        result = target_func(*args, **kwargs)
        return result

    return wraps(target_func)(_wrapper)
    #return _wrapper

@my_decorator
def my_function(x):
    """
    デこられた関数のDocstringだよ
    >>> my_function(3)
    9
    """

    return x * x


if __name__ == '__main__':
    result = my_function(3)
    print(f"result: {result}")
    print(my_function.__name__)
    print(my_function.__doc__)
    doctest.testmod()

```


## 関数型の使いどころ

djangoのコードを見ていて、以下のような部分が気になってきます。

```
user_passes_test(check_perms, login_url=login_url)(view_func)
```

一瞬理解がしづらかったですが、user_passes_testの返り値としては

def decorator(view_func):

として定義されている関数なので、
user_passes_test() で返ってきた関数オブジェクトに、view_funcを渡して実行しているということですね。ただここもコードリーディングが結構難しくなるなあという印象でした。

別の部分の実装にもありました。

```python

def login_required(
    function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None
):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    
    #ここでオブジェクトを作成
    actual_decorator = user_passes_test(
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name,
    )

    #実際の実行
    if function:
        return actual_decorator(function)
    return actual_decorator


```

こっちは一行にまとめず関数型オブジェクトの作成とreturnをバラバラに扱っている実装ですね。

:::note info

デコレーターは関数型を利用する場面が多いが、コードリーディングは難しくなると思います。コード中で書き方をどちらかに統一するのも良いかも。

:::


すごい簡単な実装例ですが、以下のような処理をしているわけですね。

```python

def outer_function(a):
    def inner_function(func):
        print(a)
        return func()
    return inner_function

def my_function():
    print("Hello")

result = outer_function(10)(my_function)

```


## まとめ

1. デコレーターでの関数の差し替え
2. wrapsの使い所 
3. 関数型の使い所