私が以下を持っているとしましょう:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
接続を設定する手間(またはデコレータが実行していること)を行わずにspam
関数をテストしたいと思います。
spam
が与えられた場合、それからデコレータを削除して、基になる「装飾されていない」関数を取得するにはどうすればよいですか?
一般的なケースではできません。
@with_connection
def spam(connection):
# Do something
に相当
def spam(connection):
# Do something
spam = with_connection(spam)
つまり、「元の」スパムが存在しなくなった可能性もあります。 (きれいすぎない)ハックは次のようになります:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
decorated._original = f
return decorated
@with_connection
def spam(connection):
# Do something
spam._original(testcon) # calls the undecorated function
この質問には少し更新がありました。 Python 3を使用している場合は、__wrapped__
stdlibのデコレータのプロパティ。
Pythonクックブック、第3版、セクション9.3の例を次に示します。デコレータのアンラップ
>>> @somedecorator
>>> def add(x, y):
... return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>
カスタムデコレータから関数をアンラップしようとする場合、デコレータ関数はwraps
のfunctools
関数を使用する必要があります。 Python Cookbook、3rd edition、section 9.2 Preservingデコレータを書くときの関数メタデータ
>>> from functools import wraps
>>> def somedecoarator(func):
... @wraps(func)
... def wrapper(*args, **kwargs):
... # decorator implementation here
... # ...
... return func(*args, kwargs)
...
... return wrapper
balphaのソリューションは、このメタデコレーターでより一般化できます。
def include_original(dec):
def meta_decorator(f):
decorated = dec(f)
decorated._original = f
return decorated
return meta_decorator
次に、@ include_originalを使用してデコレーターを装飾できます。デコレーターには、テスト可能な(装飾されていない)バージョンが組み込まれています。
@include_original
def shout(f):
def _():
string = f()
return string.upper()
return _
@shout
def function():
return "hello world"
>>> print function()
HELLO_WORLD
>>> print function._original()
hello world
見よ、FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:
orig_spam = spam.func_closure[0].cell_contents
編集:2回以上装飾され、より複雑なデコレーターで装飾された関数/メソッドの場合、次のコードを使用して試すことができます。装飾された関数は元の関数とは異なる__name__dであるという事実に依存しています。
def search_for_orig(decorated, orig_name):
for obj in (c.cell_contents for c in decorated.__closure__):
if hasattr(obj, "__name__") and obj.__== orig_name:
return obj
if hasattr(obj, "__closure__") and obj.__closure__:
found = search_for_orig(obj, orig_name)
if found:
return found
return None
>>> search_for_orig(spam, "spam")
<function spam at 0x027ACD70>
しかし、それは馬鹿な証拠ではありません。デコレータから返された関数の名前が装飾されたものと同じである場合、失敗します。 hasattr()チェックの順序もヒューリスティックです。どのような場合でも間違った結果を返す装飾チェーンがあります。
ndecorated パッケージを使用できるようになりました:
>>> from undecorated import undecorated
>>> undecorated(spam)
さまざまなデコレータのすべてのレイヤを掘り下げる手間を省いて、底部の機能に到達し、元のデコレータを変更する必要がありません。 Python 2とPython 3。
代わりに...
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
orig_spam = magic_hack_of_a_function(spam)
あなたはただすることができます...
def with_connection(f):
...
def spam_f(connection):
...
spam = with_connection(spam_f)
...これが@decorator
構文のすべてです。これで、元のspam_f
に通常どおりにアクセスできます。
そのような関数をテストする通常のアプローチは、get_connectionなどの依存関係を構成可能にすることです。その後、テスト中にモックでオーバーライドできます。基本的にJavaの世界での依存性注入と同じですが、Pythonの動的な性質のおかげではるかに単純です。
そのためのコードは次のようになります。
# decorator definition
def with_connection(f):
def decorated(*args, **kwargs):
f(with_connection.connection_getter(), *args, **kwargs)
return decorated
# normal configuration
with_connection.connection_getter = lambda: get_connection(...)
# inside testsuite setup override it
with_connection.connection_getter = lambda: "a mock connection"
コードによっては、ファクトリー関数を使用するためのデコレーターよりも優れたオブジェクトを見つけることができます。それをデコレータに置くことの問題は、ティアダウンメソッドで古い値に戻すことを忘れないようにする必要があることです。
元の関数はspam.__closure__[0].cell_contents
に格納されています。
Decoratorはクロージャを使用して、元の機能を追加の機能層にバインドします。元の関数は、デコレータの入れ子構造の関数の1つが保持するクロージャセルに保存する必要があります。
例:
>>> def add(f):
... def _decorator(*args, **kargs):
... print('hello_world')
... return f(*args, **kargs)
... return _decorator
...
>>> @add
... def f(msg):
... print('f ==>', msg)
...
>>> f('alice')
hello_world
f ==> alice
>>> f.__closure__[0].cell_contents
<function f at 0x7f5d205991e0>
>>> f.__closure__[0].cell_contents('alice')
f ==> alice
これは ndecorated の中心的な原則です。詳細については、ソースコードを参照してください。
次のように、デコレータを functools.wraps
で装飾することをお勧めします。
import functools
def with_connection(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
Python 3.2以降、これにより、元の装飾されていない関数を取得できる__wrapped__
属性が自動的に追加されます。
>>> spam.__wrapped__
<function spam at 0x7fe4e6dfc048>
ただし、__wrapped__
属性に手動でアクセスする代わりに、 inspect.unwrap
を使用することをお勧めします。
>>> inspect.unwrap(spam)
<function spam at 0x7fe4e6dfc048>
何もしないデコレータを追加します。
def do_nothing(f):
return f
With_connectionを定義またはインポートした後、デコレータとして使用するメソッドに到達する前に、以下を追加します。
if TESTING:
with_connection = do_nothing
次に、グローバルTESTINGをTrueに設定すると、with_connectionを何もしないデコレーターに置き換えます。