デコレーターとは
関数の前後に他の特定の処理を入れ込む機能です。オープンソースでも使われているとの情報を確認し、実際の実装を見ていましたが、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点がわかりづらかったので確認します。
- デコレーターでの関数の差し替え
- wrapsの使い所
- 関数型の使い所
デコレーターによる関数の差し替えの例
以下の部分で、返答する関数オブジェクトが分岐されています。
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)
まとめ
- デコレーターでの関数の差し替え
- wrapsの使い所
- 関数型の使い所