0
0

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

Last updated at Posted at 2024-08-18

デコレーターとは

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

djangoの login_required の例


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. 関数型の使い所

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

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


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)

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

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

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

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

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


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)を適切に設定

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

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

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


from functools import wraps

# django内部での実装部分

wraps(target_func)(_wrapper)

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

結論

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


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のような書き方でも同じことになります。目的は一緒と思います。


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を渡して実行しているということですね。ただここもコードリーディングが結構難しくなるなあという印象でした。

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


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をバラバラに扱っている実装ですね。

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

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


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. 関数型の使い所
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0