web-dev-qa-db-ja.com

Pythonで関数からデコレータを取り除く方法

私が以下を持っているとしましょう:

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    return decorated

@with_connection
def spam(connection):
    # Do something

接続を設定する手間(またはデコレータが実行していること)を行わずにspam関数をテストしたいと思います。

spamが与えられた場合、それからデコレータを削除して、基になる「装飾されていない」関数を取得するにはどうすればよいですか?

57
Herge

一般的なケースではできません。

@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
38
balpha

この質問には少し更新がありました。 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
>>>

カスタムデコレータから関数をアンラップしようとする場合、デコレータ関数はwrapsfunctools関数を使用する必要があります。 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
34
Alex Volkov

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
30
jcdyer

見よ、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()チェックの順序もヒューリスティックです。どのような場合でも間違った結果を返す装飾チェーンがあります。

18

ndecorated パッケージを使用できるようになりました:

>>> from undecorated import undecorated
>>> undecorated(spam)

さまざまなデコレータのすべてのレイヤを掘り下げる手間を省いて、底部の機能に到達し、元のデコレータを変更する必要がありません。 Python 2とPython 3。

8
Oin

代わりに...

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に通常どおりにアクセスできます。

6
dbr

そのような関数をテストする通常のアプローチは、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"

コードによっては、ファクトリー関数を使用するためのデコレーターよりも優れたオブジェクトを見つけることができます。それをデコレータに置くことの問題は、ティアダウンメソッドで古い値に戻すことを忘れないようにする必要があることです。

2
Ants Aasma

元の関数は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 の中心的な原則です。詳細については、ソースコードを参照してください。

2
lyu.l

次のように、デコレータを 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>
2
Aran-Fey

何もしないデコレータを追加します。

def do_nothing(f):
    return f

With_connectionを定義またはインポートした後、デコレータとして使用するメソッドに到達する前に、以下を追加します。

if TESTING:
    with_connection = do_nothing

次に、グローバルTESTINGをTrueに設定すると、with_connectionを何もしないデコレーターに置き換えます。

1
PaulMcG