functools.wrapsは何をしますか?


回答:


1069

デコレーターを使用すると、1つの関数が別の関数に置き換えられます。つまり、デコレータがある場合

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

その後、あなたが言うとき

@logged
def f(x):
   """does some math"""
   return x + x * x

それは言うこととまったく同じです

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

関数fが関数に置き換えられますwith_logging。残念ながら、これはあなたがそれから言うなら

print(f.__name__)

それはwith_loggingあなたの新しい関数の名前だからです。実際、のdocstringを見ると、docstring がないfため空白にwith_loggingなります。そのため、記述したdocstringはもう存在しません。また、その関数のpydocの結果を見ると、1つの引数を取るものとしてリストされていませんx。代わりに、それが取る*argsと表示され**kwargsます。これは、with_loggingが取るためです。

デコレータを使用すると、常に関数に関するこの情報が失われることになるので、深刻な問題になります。それが私たちが持っている理由functools.wrapsです。これは、デコレータで使用される関数を受け取り、関数名、docstring、引数リストなどをコピーする機能を追加します。wrapsそれ自体がデコレータであるため、次のコードは正しいことを行います。

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'

7
うん、functools.wrapsは標準ライブラリの一部であり、別の外部依存関係を導入しないので、私はデコレータモジュールを避けることを好みます。しかし、decoratorモジュールは実際にヘルプの問題を解決します。うまくいけばfunctools.wrapsもいつかは解決するでしょう。
Eli Courtwright、2009年

6
ラップを使用しない場合の例を次に示します。doctoolsテストが突然消えることがあります。これは、wraps()などでテストをコピーしない限り、doctoolsが修飾された関数のテストを見つけられないためです。
Andrew Cookeは、2011

88
なぜfunctools.wrapsこの仕事が必要なのですか、そもそもデコレータパターンの一部にすぎないのではないでしょうか。@wrapsを使用したくない場合
2014年

56
@wim:@wrapsコピーされた値に対してさまざまなタイプの変更または注釈を実行するために、独自のバージョンを実行するいくつかのデコレータを作成しました。基本的に、これはPythonの哲学の延長であり、明示的は暗黙的よりも優れており、特別な場合はルールを破るほど特別ではありません。(@wrapsなんらかの特別なオプトアウトメカニズムを使用するよりも、コードがはるかにシンプルになり、言語を手動で提供する必要がある場合に理解しやすくなります。)
ssokolow

35
@LucasMalorすべてのデコレータがデコレートする関数をラップするわけではありません。ある種のルックアップシステムに登録するなど、副作用を適用するものもあります。
ssokolow 2015

22

デコレーターには、関数ではなくクラスをよく使用します。オブジェクトは関数に期待されるものと同じ属性をすべて持っているわけではないので、私はこれでいくつかの問題を抱えていました。たとえば、オブジェクトには属性はありません__name__。Djangoが「オブジェクトに属性 ' __name__' がありません」というエラーを報告していたところを追跡するのが非常に困難な、これに関する特定の問題がありました。残念ながら、クラススタイルのデコレータの場合、@ wrapでうまくいくとは思いません。代わりに、次のような基本デコレータクラスを作成しました。

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

このクラスは、装飾されている関数へのすべての属性呼び出しをプロキシします。したがって、次のように2つの引数が指定されていることを確認する単純なデコレーターを作成できます。

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)

7
のドキュメントが@wraps言うように、@wrapsはの便利な関数functools.update_wrapper()です。クラスデコレータの場合update_wrapper()__init__()メソッドから直接呼び出すことができます。だから、あなたが作成する必要はありませんDecBaseあなただけに含めることができる、まったく__init__()process_login行:update_wrapper(self, func)。それで全部です。
Fabiano

14

Python 3.5以降では:

@functools.wraps(f)
def g():
    pass

のエイリアスですg = functools.update_wrapper(g, f)。それはちょうど3つのことをします:

  • それをコピー__module____name____qualname____doc__、と__annotations__の属性fg。このデフォルトのリストはにありWRAPPER_ASSIGNMENTSfunctoolsソースで確認できます。
  • のすべての要素__dict__gを更新しますf.__dict__。(WRAPPER_UPDATESソースを参照)
  • 新しい__wrapped__=f属性を設定しますg

結果はg、と同じ名前、docstring、モジュール名、および署名を持つように見えますf。唯一の問題は、署名に関してこれが実際には当てはまらないことinspect.signatureです。デフォルトでは、ラッパーチェーンに従います。ドキュメントでinspect.signature(g, follow_wrapped=False)説明されているように使用して確認できます。これは迷惑な結果をもたらします:

  • ラッパーコードは、指定された引数が無効な場合でも実行されます。
  • ラッパーコードは、受信した* args、** kwargsから、その名前を使用して引数に簡単にアクセスできません。実際、すべてのケース(位置、キーワード、デフォルト)を処理する必要があるため、のようなものを使用する必要がありますSignature.bind()

functools.wrapsデコレータを開発するための非常に頻繁なユースケースは関数をラップすることなので、とデコレータの間には少し混乱があります。しかし、どちらも完全に独立した概念です。違いを理解することに興味がある場合は、両方にヘルパーライブラリを実装しました。デコパッチを簡単に作成するためのdecopatchと、シグネチャを保持するの代替を提供するmakefunです@wrapsmakefun有名なdecoratorライブラリと同じ実績のあるトリックに依存していることに注意してください。


3

これはラップに関するソースコードです:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

2
  1. 前提条件:デコレーターの使用方法、特にラップの使用方法を知っている必要があります。このコメントはそれを少し明確に説明するか、このリンクもそれをかなりよく説明しています。

  2. For egを使用するときはいつでも:@wrapsの後に独自のラッパー関数が続きます。このリンクに記載されている詳細に従って、それは言う

functools.wrapsは、ラッパー関数を定義するときに、関数デコレーターとしてupdate_wrapper()を呼び出すための便利な関数です。

これは、partial(update_wrapper、wrapped = wrapped、assigned = assigned、updated = updated)と同等です。

したがって、@ wrapsデコレータは、実際にはfunctools.partial(func [、* args] [、** keywords])を呼び出します。

functools.partial()の定義は、

partial()は、関数の引数またはキーワード、あるいはその両方の一部を「フリーズ」して、簡略化されたシグネチャを持つ新しいオブジェクトを生成する部分的な関数アプリケーションに使用されます。たとえば、partial()を使用して、基本引数のデフォルトが2であるint()関数のように動作する呼び出し可能オブジェクトを作成できます。

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

@wrapsはpartial()を呼び出し、ラッパー関数をパラメーターとして渡します。最後のpartial()は、簡易バージョン、つまり、ラッパー関数自体ではなく、ラッパー関数内にあるもののオブジェクトを返します。


-4

つまり、functools.wrapsは単なる通常の関数です。この公式の例を考えてみましょう。助けを借りて、ソースコード、我々は実装し、次のように実行する手順の詳細を見ることができます:

  1. wraps(f)はオブジェクト、たとえばO1を返します。これはクラスPartialのオブジェクトです
  2. 次のステップは@ O1 ...で、これはPythonのデコレータ表記です。その意味は

wrapper = O1 .__ call __(ラッパー)

実装チェック__call__を、我々はこの段階、(左側)の後にそれを参照してくださいラッパーオブジェクトがでたなり(* self.args、* argsを、** newkeywords)self.funcの作成チェックO1をして__new__、我々self.funcが関数update_wrapperであることを知ってください。最初のパラメーターとして、パラメーター* args(右側のラッパー)を使用します。update_wrapperの最後のステップを確認すると、右側のラッパーが返され、必要に応じて一部の属性が変更されています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.