質問
非常に一般的なことを行うデコレータを書いたとします。たとえば、すべての引数を特定の型に変換したり、ログを記録したり、メモ化を実装したりする可能性があります。
例を次に示します。
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を使用して、装飾された関数をどのように構築すればよいですか?
その他のアプローチ?
解決
-
装飾モジュールをインストールします。
$ pip install decorator
-
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番目のオプション:
- 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
に関するこの投稿も参照してください。