【Python 雑談・雑学】 デコレータ (decorators) を理解しよう - デコレータ、オリジナル関数からの引数の渡し方、受け取り方 投稿一覧へ戻る

Tags: miscellaneous , decorator , python

Published 2020年6月23日21:27 by T.Tsuyoshi

デコレーター (decorators) は関数を引数として受け取り、他の関数に置き換えた上で、最終的にオリジナルの関数を実行します。
他の関数に置き換えることで、オリジナルの関数の機能を補完、変更することが可能になります。


簡単な実装方法からよりジェネリックで現実的な実装方法までを段階を追って見ていきましょう。


ユーザーが admin パーミッションを持っていれば admin サイトへログインするためのパスワードを表示する関数を作成します。
このとき、ユーザーが admin パーミッションを持っているかどうかの判断は他の関数 (これがデコレータになります) に委ねましょう。


1st step - デコレータの基礎となる形

def user_has_permission1(func):
if user.get('access_level') == 'admin':
return func
raise RuntimeError


def my_function1():
return 'Password for admin panel is 1234.'


user = {'username': 'Nana', 'access_level': 'admin'}


my_secure_function1 = user_has_permission1(my_function1) # my_secure_function1 変数には結果的に my_function1 が入ります
print(my_secure_function1()) # Password for admin panel is 1234.



この実装では、オリジナルの関数 (my_function1) が直接返ってきているだけですね。
通常のデコレータでは、機能追加等が容易にできるようにオリジナルの関数をラップして実行するもう1つの関数を設けています。


2nd step - 通常のデコレータの実装
def user_has_permission2(func):
def secure_func2():
if user2.get('access_level') == 'admin':
return func()
return secure_func2


def my_function2():
return 'Password for admin panel is 1234.'


user2 = {'username': 'Nana', 'access_level': 'admin'}


my_secure_function2 = user_has_permission2(my_function2) # my_secure_function2 変数には secure_func2 が入ります
print(my_secure_function2()) # Password for admin panel is 1234.



1st step, 2nd step における問題点は、相変わらずオリジナルの関数 (my_functionX) がむき出しになってしまっており、誰でも実行できてしまうことです。
そこで、my_functionX を利用する際には必ず user_has_permissionX を呼び出すようにします。


3rd step - @ シンタックスによるデコレータの指定
def user_has_permission3(func):
def secure_func3():
if user3.get('access_level') == 'admin':
return func()
return secure_func3


@user_has_permission3
def my_function3():
"""
my_function3: adminサイトにログインするためのパスワードを提供する
"""
return 'Password for admin panel is 1234.'


user3 = {'username': 'Nana', 'access_level': 'admin'}

print(my_function3()) # Password for admin panel is 1234.


# access_level を guest に変更してみます
user3 = {'username': 'Nana', 'access_level': 'guest'}

print(my_function3()) # None



ここでお馴染みの @ シンタックスが登場します。
これは、「my_function3 の実行時には必ず user_has_permission3(my_function3) を実行してください」ということを宣言するものです。
ですから、2nd step までは必要だった

my_secure_functionX = user_has_permissionX(my_functionX)

という代入ステートメントは必要なくなり、my_function3() を実行するだけですべてが完結します。


ここでちょっとした問題が発生します。


print(my_function3.__name__)



これを実行してみると secure_func3 が返ってきます。
my_function3 がデコレータに渡され内部関数でラップされた結果、my_function3 を実行することは、イコール、secure_func3 を実行することになっている結果です。
さらに下記のコードを実行してみると...


print(my_function3.__doc__) # None
help(my_function3)

# Help on function secure_func3 in module __main__:
# secure_func3()




my_function3 に関する情報は何ひとつ得られません。


さらに、@user_has_permission3 で修飾された関数がほかにもあると...


@user_has_permission3
def another_function3():
pass

print(another_function3.__name__) # secure_func3



これでは困りますね。
いくら同じデコレータで修飾されているとはいっても、オリジナルの関数は全然別のものです。


そこで「secure_func3 は他の関数 func をラップして機能を拡張している関数なので、問い合わせに対してはオリジナルの関数の情報を渡してください」ということを Python に依頼します。


4th step - オリジナル関数の情報の取得
from functools import wraps


def user_has_permission4(func):
@wraps(func)
def secure_func4():
if user4.get('access_level') == 'admin':
return func()
return secure_func4


@user_has_permission4
def my_function4():
"""
my_function4: adminサイトにログインするためのパスワードを提供する
"""
return 'Password for admin panel is 1234.'


@user_has_permission4
def another_function4():
"""
another_function4: 何やらほかの事をやる
"""
pass


print(my_function4.__name__) # my_function4
print(my_function4.__doc__) # my_function4: adminサイトにログインするためのパスワードを提供する
print(another_function4.__name__) # another_function4
print(another_function4.__doc__) # another_function4: 何やらほかの事をやる



期待通りですね。ちゃんとオリジナル関数の情報が取得できています。
デコレータを自作した場合、@functools.wraps() デコレータを常に使用するように習慣付けたほうがいいでしょう。


さて、ここで、ユーザーの access_level が admin の場合には、admin サイトだけではなくて special サイトにアクセスするためのパスワードも取得できるようにしましょう。
そのために、どちらのサイトのパスワードが必要かを示す引数を my_functionX に渡すようにします。


5th step - オリジナル関数が引数を必要とする際のデコレータ側の対応
def user_has_permission5(func):
@wraps(func)
def secure_func5(panel_name): # 3
if user5.get('access_level') == 'admin':
return func(panel_name) # 4
return secure_func5


@user_has_permission5 # 2
def my_function5(panel_name):
"""
my_function5: adminサイト もしくは specialサイト にログインするためのパスワードを提供する
"""
password = 1234
if panel_name == 'special':
password = 4321

return f"Password for {panel_name} panel is {password}."


user5 = {'username': 'Nana', 'access_level': 'admin'}


print(my_function5('admin')) # 1

# Password for admin panel is 1234.




print(my_function5('special'))

# Password for special panel is 4321.




ここまでの復習もかねて上のコードを追いかけてみましょう。

1: 引数 'admin' を渡して my_function5() を呼び出します。
2: @user_has_permission5 デコレータによって my_function5 は user_has_permission5() の引数として渡され、その返却値は secure_func5 ですから、結果的に my_function5 は secure_func5 に入れ替わります。
3: secure_func5 は my_function5 の引数をそのまま引き継ぎます。
4: secure_func5 が実行され条件が満たされた場合はオリジナルの関数である my_function5 (== func) に引数が渡されて実行されます。


しかし、ここでまた問題が発生します。
この変更によって、@user_has_permission5 で修飾される関数は、常に引数が1つ必要になってしまいます。


ユーザーが admin パーミッションを有している場合は XXXX を実行したいが引数は必要ない、という次のような関数での利用はエラーになります。


@user_has_permission5
def another5():
return 'Hello!'

print(another5()) # TypeError: secure_func5() missing 1 required positional argument: 'panel_name'



もし無理矢理引数を渡したとしても、デコレータ関数内では最終的に another5() が実行され、その another5() 自体が引数を取らないのですから、やっぱりエラーになってしまいます。


print(another5('admin')) # TypeError: another() takes 0 positional arguments but 1 was given



これはよろしくありません。もっとジェネリックなものにすれば用途が広がります。
そのためには、secure_func5 が任意の数の positional arguments、keyword arguments を取るようにすればいいですね。
そうです、*args、**kwargs の出番です。


6th step - デコレータをよりジェネリックにする
def user_has_permission6(func):
@wraps(func)
def secure_func6(*args, **kwargs):
if user6.get('access_level') == 'admin':
return func(*args, **kwargs)
return secure_func6


@user_has_permission6
def my_function6(panel_name):
"""
my_function6: adminサイト もしくは specialサイト にログインするためのパスワードを提供する
"""
if panel_name == 'admin':
password = 1234
elif panel_name == 'special':
password = 4321
else:
password = 0

return f"Password for {panel_name} panel is {password}."


@user_has_permission6
def another6():
return 'Hello!'


user6 = {'username': 'Nana', 'access_level': 'admin'}


print(my_function6('admin')) # Password for admin panel is 1234.
print(another6()) # Hello!



この変更によって、@user_has_permissionX で修飾された関数が引数を取ろうが取るまいが1つのデコレータで対応できるようになりました。


しかし、人間の欲は限度を知りません。ここで、またまた不満が出てきました。
現在デコレータ関数内でオリジナル関数をラップしている secure_funcX では、userX.get('access_level') == 'admin'、と、
比較するアクセスレベル ('admin') をハードコーティングしています。


ですが、デコレータ内で比較するアクセスレベルを任意で指定できるようにしたい、そうすればより一層このデコレータがジェネリックになるじゃないか、というんです。
今度はデコレータ自体が引数を取るようにしたい、ということですね。
つまり、ある時は、


@user_has_permissionX('admin')
def my_functionX('special'):
...



またある時は、


@user_has_permissionX('user')
def my_functionX('discount'):
...



というように指定できるようにしたいんです。でもどうやってデコレータ自体に引数を渡したらいいんでしょう?


さて、ここからはちょっと頭の体操です。
デコレータに引数を渡しているこの構文をもう一度見てください。


@user_has_permissionX('user')



先頭の @ を除けば function_name() です、これはまさに Python の関数を呼び出す際の構文です。
ですが、6th step のコードを見てください。
def my_function6(panel_name) を修飾している @user_has_permission6 の実体は user_has_permission6(func) であり間違いなく関数呼び出しですが () がついていません。
実は、これがまさに @ シンタックスが我々の代わりにやってくれていることなんです。
すなわち @user_has_permissionX がやっていることは user_has_permissionX(func) という関数呼び出しなんです。


それでは @user_has_permission('user') はどう解釈されるでしょう?
そうです、user_has_permission('user')(func) となるんです。


これはどういうことでしょうか?
1: user_has_permission('user') という関数呼び出しがあって、
2: その関数の戻り値は、デコレータで修飾されているオリジナル関数を引数として取る関数である、ということです。


さぁ、頭が痛くなって参りました。
では、これをどう実装したらいいのでしょうか?


def most_outer(access_level):
def user_has_permission6(func): # 4
@wraps(func)
def secure_func6(*args, **kwargs):
if user6.get('access_level') == access_level:
return func(*args, **kwargs) # 7
return secure_func6 # 5
return user_has_permission6 # 2


@most_outer('admin') # 1, 3
def my_function6(panel_name): # 6
...


print(my_function6('admin')) # 0, 8



6th step のデコレータ関数全体を他の関数でラップしました。
さて、追いかけてみましょう。


0: my_function6 に引数を渡して呼び出します。
1: my_function6 を修飾している @most_outer('admin') において most_outer('admin') の部分は通常の関数呼び出しですから @ シンタックスが機能する前に実行されます。
2: most_outer('admin') が実行されて user_has_permission6 関数が返されてきます。
3: 1 と 2 の実行に伴い、@most_outer('admin') は @user_has_permission6 に置き換えられます。おやっ、これはすでに見慣れた通常のデコレータの書式です。
4: @ シンタックスは我々の代わりにオリジナル関数を引数として関数呼び出しをしてくれますから、user_has_permission6(my_function6) が実行されます。
5: 4 の結果として secure_func6 が返されてきます。
6: 5 の結果として my_function6(panel_name) は secure_func6(panel_name) と解釈されて実行されます。
7: 最終的にオリジナル関数である my_function6(panel_name) が実行されて、
8: 結果が表示されます。


つまるところ、


user_has_permission6 = most_outer('admin')
secure_func6 = user_has_permission6(my_function6)



というように関数が入れ替わっていく、ということですね。まとめてみましょう。
役割を明確にするために関数名を入れ替えてあります、注意して見て下さい。


7th step - デコレータをもっともっとジェネリックにする
def user_has_permission7(access_level):
def my_decorator7(func):
@wraps(func)
def secure_func7(*args, **kwargs):
if user7.get('access_level') == access_level:
return func(*args, **kwargs)
return secure_func7
return my_decorator7


@user_has_permission7('admin')
def my_function7(panel_name):
"""
my_function7: admin パーミッションがあるユーザーに対して adminサイト もしくは specialサイト にログインするためのパスワードを提供する
"""
if panel_name == 'admin':
password = 1234
elif panel_name == 'special':
password = 4321
else:
password = 0

return f"Password for {panel_name} panel is {password}."


@user_has_permission7('user')
def user_discount7():
"""
user_discount7: user パーミッションのユーザーに対して割引価格を提示する
"""
return "500円のところ 498円の大特価でご提供!"


user7 = {'username': 'Nana', 'access_level': 'admin'}


print(my_function7('admin')) # Password for admin panel is 1234.
print(user_discount7()) # None


user7 = {'username': 'Nana', 'access_level': 'user'}


print(my_function7('admin')) # None
print(user_discount7()) # 500円のところ 498円の大特価でご提供!

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)