今回やる事
フレームワークや至る所で、このfunctoolsのwrapsはけっこう出てくる上、意味不明だったのでここらで理解したいと思い、追ってみました。
wrapsとは何か
公式ドキュメントは以下のように言ってます。
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
これはラッパー関数を定義するときに update_wrapper() を関数デコレータとして呼び出す便宜関数です。これは partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) と等価です。
この説明から、wrapsは__partialオブジェクトを返すデコレーター__みたいです。
partialオブジェクトとは?
partialは、クラスで実装されたデコレータとなっているようです。
英語の意味からすると部品みたいなものですかね。
update_wrapperとは?
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
wrapper 関数を wrapped 関数に見えるようにアップデートします。
その名の通り、wrapper関数をアップデートする関数のようです
内部の実装は、assignedとupdatedで指定された属性のタプルをループし、
wrapped(wrapsに渡した関数)のものをwrapper(wrapsを付けた関数)にセットする処理になっているようです。
↓
つまり、__wrapsに渡したオブジェクトの情報で、wrapsを付けたオブジェクトの属性らを上書きする動きをするもの__のようです。
デフォルトでアップデートされる属性は以下の通りになっています。
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
クラスで実装したデコレータの動きの確認
ここでは、partialクラスの動きを把握するために、クラスでデコレータを作ったらどういう流れで動くかを確認してみました。
class deco:
def __new__(cls, func, *args, **kwargs):
print('====new====') # 1. まずnewが動く。
print('func => ', func.__name__)
self = super().__new__(cls)
self.func = func
return self
def __init__(self, *args, **kwargs):
print('====init====') # 2. 次にinitが動く
print('func => ', self.func.__name__)
def __call__(self, *args, **kwargs):
print('====call====') # 3. 次にcallが動く
return self.func(*args, **kwargs)
@deco
def func():
print('funcだよ')
func()
====new====
func => func
====init====
func => func
====call====
funcだよ
クラスで作ったデコレータは単純に、
- インスタンス化された後
- callが呼ばれる
事が分かりました。
##実装を見てみる
wrapsデコレータの実装
wrapsデコレータは単純にpartialクラスを返す関数になっている。
def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES):
"""
ラッパー関数にupdate_wrapper関数を適応するためのデコレータファクトリー
wrapped: ラッパー関数
WRAPPER_ASSIGNMENTS:
('__module__', '__name__', '__qualname__', '__doc__' __annotations__')
WRAPPER_UPDATES:
('__dict__',)
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
partialクラスの実装
このクラスが実質的なデコレータで、このクラスにupdate_wrapperメソッドと、ラップ関数を渡す事でupdate_wrapperメソッドにより、ラップ関数が更新される。
上で確認したように、クラスでデコレータを実装すると、以下の手順で動いた。
- インスタンス化されて、__new__ → __init__ が動く
- __call__ が動く
__new__メソッド
__new__メソッドはインスタンス化前に呼ばれるメソッドで、このメソッドでやらなきゃいけない事は以下の2つ。
- インスタンス化をする
- インスタンスをreturnする
class partial:
def __new__(cls, func, /, *args, **keywords):
...
if hasattr(func, 'func'):
args = func.args + args
keywords = {**func.keywords, **keywords}
func = func.func
# インスタンス化
# 省略してself = super().__new__(cls)でも良い
self = super(partial, cls).__new__(cls)
self.func = func
self.args = args
self.keywords = keywords
# 自身を返す
return self
newメソッド内では、wraps関数から受け取ったupdate_wrapper関数と、ラッパー関数などを自身にセットしておく処理が実装されている。
__call__メソッド
class partial:
def __call__(self, /, *args, **keywords):
"""
self.func:
__new__でセットしたupdate_wrapper関数
self.args:
ラッパー関数
args:
ラップされた関数
"""
keywords = {**self.keywords, **keywords}
return self.func(*self.args, *args, **keywords)
インスタンスの初期化が終わったら、callメソッドが呼ばれる。
このcallメソッド引数にはデコレートした関数などが入ってくる。
callメソッド内では、最終的にupdate_wrapper関数が呼ばれる。
update_wrapper関数
def update_wrapper(wrapper, wrapped,
assigned=WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES):
"""
1. wrapper:
@wrapsに渡した関数
2. wrapped:
@wrapsを付けた関数
WRAPPER_ASSIGNMENTS:
('__module__', '__name__', '__qualname__', '__doc__' __annotations__')
WRAPPER_UPDATES:
('__dict__',)
"""
# wrappedの該当アトリビュートを取得し、wrapperの該当アトリビュートに上書きする。
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
# 2. wrapperのアトリビュートをwrappedのアトリビュートで上書きする。
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# __wrapped__属性にwrapped関数を持っておく
wrapper.__wrapped__ = wrapped
return wrapper
この関数によって属性の上書きが行われている。
各属性のupdateを行った後、__wrapped__という属性にwrapsで渡したオブジェクトを指定したりしている。
wrapsの挙動の確認
確認しやすくするために、簡単なクロージャーで確認してみます。
とりあえず、__name__と__dict__がどうなっているか見てみます。
wrapsを付けない際の挙動
def outer(func):
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
class Foo:
a = 1
def __call__(self):
pass
func1 = outer(Foo)
func2 = outer(func1)
print('func1 name: ', func1.__name__)
print('func1 dict: ', func1.__dict__)
print('func2 name: ', func2.__name__)
print('func2 dict: ', func2.__dict__)
func2()
func1 name: inner
func1 dict: {}
func2 name: inner
func2 dict: {}
デフォルトの動きでは、outerメソッドにFooクラスを渡したのにもかかわらずFooの属性などは記憶されないで、inner関数の属性で上書きされているようです。
wrapsを付けた場合の挙動
from functools import wraps
def outer(func):
@wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
class Foo:
a = 1
def __call__(self):
pass
func1 = outer(Foo)
func2 = outer(func1)
print('func1 name: ', func1.__name__)
print('func1 dict: ', func1.__dict__)
print('func2 name: ', func2.__name__)
print('func2 dict: ', func2.__dict__)
func2()
func1 name: Foo
func1 dict: {
'__module__': '__main__',
'a': 1,
'__call__': <function Foo.__call__ at 0x000001DAA41D40D0>,
'__dict__': <attribute '__dict__' of 'Foo' objects>,
'__weakref__': <attribute '__weakref__' of 'Foo' objects>,
'__doc__': None,
'__wrapped__': <class '__main__.Foo'>
}
func2 name: Foo
func2 dict: {
'__module__': '__main__',
'a': 1,
'__call__': <function Foo.__call__ at 0x000001DAA41D40D0>,
'__dict__': <attribute '__dict__' of 'Foo' objects>,
'__weakref__': <attribute '__weakref__' of 'Foo' objects>,
'__doc__': None,
'__wrapped__': <function Foo at 0x000001DAA41D4550>
}
wrapsを付けたことによって、inner関数をFooクラスの属性で上書きし、inner関数からそれらにアクセスする事が出来るようになりました。
これによって多重にデコレーターでデコっても、各属性にアクセスする事が出来そうです。
Djangoでの実装を見てみる
Djangoでは、数ヶ所で使われていますが、ここではミドルウェアチェーン作成のところで使用されているメソッドを参考にします。
このメソッドは、リクエストを処理する関数と、複数のミドルウェアをチェーンみたいに繋ぎ合わせるために使用されるメソッドで、引数にリクエストを処理する関数と、複数のミドルウェアが渡ってきます。
wrapsを付ける事により、それらの属性に正しくアクセス出来るようにしているものと思われます。
def convert_exception_to_response(get_response):
if asyncio.iscoroutinefunction(get_response):
@wraps(get_response)
async def inner(request):
try:
response = await get_response(request)
except Exception as exc:
...
return response
return inner
else:
@wraps(get_response)
def inner(request):
try:
response = get_response(request)
except Exception as exc:
...
return response
return response
##まとめ
・通常だと、クロージャーはエンクロージャーに渡したオブジェクトの属性を記憶していない。
↓
・wrapsを付ける事により、エンクロージャーに渡したオブジェクトの属性でクロージャーの属性を上書きする。
そうする事によって、渡したオブジェクトの各属性にアクセスしたりする事が出来る。
つまり、wrapsを付けておいた方が、それぞれの情報にアクセス出来るし、とりあえず付けておけばいいと思われる。