質問

非常に一般的なことを行うデコレータを書いたとします。たとえば、すべての引数を特定の型に変換したり、ログを記録したり、メモ化を実装したりする可能性があります。

例を次に示します。

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

これまでのところすべてが順調です。ただし、1つの問題があります。装飾された関数は、元の関数のドキュメントを保持しません:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

幸いなことに、回避策があります:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

今回は、関数名とドキュメントが正しい:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

しかし、まだ問題があります。関数の署名が間違っています。情報" * args、** kwargs"役に立たない。

何をする? 2つの簡単だが欠陥のある回避策を考えることができます:

1-docstringに正しい署名を含めます:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

これは重複のために悪いです。署名は、自動生成されたドキュメントでは適切に表示されません。関数を更新してdocstringの変更を忘れたり、タイプミスをしたりするのは簡単です。 [そして、はい、docstringはすでに関数本体を複製しているという事実を知っています。これを無視してください。 funny_functionはランダムな例です。]

2-デコレータを使用しない、または特定のシグネチャごとに専用のデコレータを使用します:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

これは、同一のシグネチャを持つ一連の関数に対して正常に機能しますが、一般的には役に立ちません。冒頭で述べたように、デコレータを完全に汎用的に使用できるようにしたいです。

完全に一般的で自動化されたソリューションを探しています。

質問は次のとおりです。装飾された関数の署名を作成後に編集する方法はありますか?

そうでない場合、関数のシグネチャを抽出し、その情報を" * kwargs、** kwargs"の代わりに使用するデコレータを作成できますか?装飾された関数を構築するとき?その情報を抽出するにはどうすればよいですか? execを使用して、装飾された関数をどのように構築すればよいですか?

その他のアプローチ?

役に立ちましたか?

解決

  1. 装飾モジュールをインストールします。

    $ pip install decorator
    
  2. args_as_ints()の定義を調整:

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z
    

Python 3.4以降

stdlibの functools.wraps() はPython 3.4以降の署名を保持します:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps()が利用可能少なくともPython 2.5以降ですが、署名はそこに保存されません:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

注意: x、y、z = 3 の代わりに * args、** kwargs

他のヒント

これは、Pythonの標準ライブラリ functools 、具体的には functools.wraps 関数は、「ラップされた関数のようにラッパー関数を更新する」ように設計されています。ただし、以下に示すように、動作はPythonバージョンに依存します。質問の例に適用すると、コードは次のようになります。

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Python 3で実行すると、次の結果が生成されます。

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

唯一の欠点は、Python 2では関数の引数リストが更新されないことです。 Python 2で実行すると、次のものが生成されます。

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

デコレーターモジュール decorator 使用できるデコレータ:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

その後、メソッドの署名とヘルプが保持されます:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

編集:J. F.セバスチャンは、 args_as_ints 関数を変更しなかったことを指摘しました-現在修正されています。

decorator モジュール、特に<この問題を解決するhref = "http://www.phyast.pitt.edu/~micheles/python/documentation.html#decorator-is-a-decorator" rel = "noreferrer"> decorator デコレーター。

2番目のオプション:

  1. wraptモジュールのインストール:

$ easy_install wrapt

wraptにはボーナスがあり、クラスの署名を保持します。


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z

上記の jfsの回答でコメントしたとおり。外観( help 、および inspect.signature )で署名に関心がある場合は、 functools.wraps を使用しても問題ありません。

振る舞いの面で署名に関心がある場合(特に引数の不一致の場合の TypeError )、 functools.wraps はそれを保存しません。そのためには、 decorator を使用するか、 makefun

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

functools.wraps に関するこの投稿も参照してください。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top