web-dev-qa-db-ja.com

装飾された関数のシグネチャを保持する

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

次に例を示します。

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を使用して、装飾された関数をどのように構築する必要がありますか?

他のアプローチはありますか?

104
  1. インストール decorator モジュール:

    _$ 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() from 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_。

74
jfs

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

decorator module があり、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関数-これは修正されました。

9
DzinX

decorator モジュール、特にこの問題を解決する decorator デコレーターを見てください。

8
Brian

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
6
macm

上記の 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
smarie