装飾された関数の署名を保持する


111

非常に一般的なことを行うデコレータを書いたとしましょう。たとえば、すべての引数を特定の型に変換したり、ロギングを実行したり、メモ化を実装したりできます。

次に例を示します。

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
「時代遅れ」とは言わなかった。inspect.Signature装飾された関数を処理するために何が追加されたのか、多かれ少なかれ疑問に思いました。
NightShadeQueen 2015

回答:


78
  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以降

functools.wraps()stdlibから、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

通知:の*args, **kwargs代わりにx, y, z=3


あなたのものは最初の答えではありませんでしたが、これまでのところ最も包括的なものです:-)私は実際にはサードパーティのモジュールを含まないソリューションを好みますが、デコレータモジュールのソースを見ると、私ができるほど簡単ですコピーしてください。
Fredrik Johansson、

1
@MarkLodato:(functools.wraps()回答で述べたように)Python 3.4以降の署名はすでに保持されています。wrapper.__signature__以前のバージョンで設定が役立つことを意味しますか?(どのバージョンをテストしましたか?)
jfs

1
@MarkLodato:help()Python 3.4で正しい署名を表示します。functools.wraps()IPythonではなく、なぜ壊れていると思いますか?
jfs

1
@MarkLodato:それを修正するためのコードを書かなければならない場合は壊れています。それhelp()が正しい結果を生み出すとすれば、問題はどのソフトウェアを修正すべきfunctools.wraps()か、つまりIPythonかということです。いずれにせよ、手動で割り当てること__signature__はせいぜい回避策です-長期的な解決策ではありません。
jfs

1
ルックスは好きinspect.getfullargspec()のための適切な署名を返さない、まだfunctools.wrapsのpython 3.4で、あなたが使用しなければならないことをinspect.signature()代わりに。
Tuukka Mustonen

16

これは、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

Sphinxかどうかはわかりませんが、ラップされた関数がクラスのメソッドである場合は機能しないようです。Sphinxは引き続きデコレータの呼び出し署名を報告します。
alphabetasoup

9

使用できるデコレータ付きのデコレータモジュールがあり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

編集:JF Sebastianは、args_as_ints関数を変更しなかったと指摘しました-これは修正されました。



6

2番目のオプション:

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

$ easy_install 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

2

上記の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も参照してください。


1
また、をinspect.getfullargspec呼び出しても結果は保持されませんfunctools.wraps
laike9m

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