4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

pythonのfunctools.wrapsについて

Posted at

今回やる事

フレームワークや至る所で、この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を付けたオブジェクトの属性らを上書きする動きをするもの__のようです。


デフォルトでアップデートされる属性は以下の通りになっています。

functools.py
WRAPPER_ASSIGNMENTS  = ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
WRAPPER_UPDATES = ('__dict__',)

クラスで実装したデコレータの動きの確認

ここでは、partialクラスの動きを把握するために、クラスでデコレータを作ったらどういう流れで動くかを確認してみました。

kakunin.py
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()
output.sh
====new====
func => func

====init====
func => func

====call====

funcだよ

クラスで作ったデコレータは単純に、

  1. インスタンス化された後
  2. callが呼ばれる

事が分かりました。

##実装を見てみる

wrapsデコレータの実装

wrapsデコレータは単純にpartialクラスを返す関数になっている。

functools.py
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メソッドにより、ラップ関数が更新される。

上で確認したように、クラスでデコレータを実装すると、以下の手順で動いた。

  1. インスタンス化されて、__new__ → __init__ が動く
  2. __call__ が動く

__new__メソッド

__new__メソッドはインスタンス化前に呼ばれるメソッドで、このメソッドでやらなきゃいけない事は以下の2つ。

  1. インスタンス化をする
  2. インスタンスをreturnする
functools.py
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__メソッド

functools.py
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関数

functools.py
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を付けない際の挙動

nowraps.py
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()             
output.sh
func1 name:  inner
func1 dict:  {}

func2 name:  inner
func2 dict:  {}

デフォルトの動きでは、outerメソッドにFooクラスを渡したのにもかかわらずFooの属性などは記憶されないで、inner関数の属性で上書きされているようです。

wrapsを付けた場合の挙動

wraps.py
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()                
output.sh
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を付ける事により、それらの属性に正しくアクセス出来るようにしているものと思われます。

django.core.handlers.exceptions.py
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を付けておいた方が、それぞれの情報にアクセス出来るし、とりあえず付けておけばいいと思われる。

4
4
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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?