1. ホーム
  2. python

[解決済み] パラメータ付きでもパラメータなしでも使えるデコレータを作るには?

2022-10-05 09:35:55

質問

Pythonのデコレータを作りたいのですが、パラメータを付けて使うこともできます。

@redirect_output("somewhere.log")
def foo():
    ....

を使うか、使わないか(例えば、デフォルトで出力を標準エラー出力にリダイレクトする)です。

@redirect_output
def foo():
    ....

それは全く可能なのでしょうか?

私は出力をリダイレクトする問題に対する別の解決策を探しているわけではなく、私が実現したい構文の一例に過ぎないことに注意してください。

どのように解決するのですか?

この質問が古いことは知っていますが、コメントのいくつかは新しいもので、実行可能な解決策はすべて基本的に同じですが、それらのほとんどはあまりきれいでなく、読みやすくもありません。

thobe の回答が言うように、両方のケースを処理する唯一の方法は、両方のシナリオをチェックすることです。 最も簡単な方法は、単一の引数があり、それが callabe であるかどうかを確認することです (注: あなたのデコレーターが 1 つの引数だけを取り、それがたまたま callable オブジェクトである場合、追加のチェックが必要になるでしょう)。

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

最初のケースでは、通常のデコレータが行うように、渡された関数を修正またはラップしたものを返します。

2つ目のケースでは、*args, **kwargs で渡された情報を何らかの形で使用する「新しい」デコレータを返します。

これはこれでいいのですが、デコレータを作るたびにそれを書き出さなければならないのは、かなり煩わしいですし、きれいなものではありません。 その代わりに、デコレータを書き直すことなく自動的に修正できるようになるといいのですが...そのためのデコレータなのです!

次のデコレータを使用すると、デコレータを引数つきでも引数なしでも使用できるようにすることができます。

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

さて、@doublewrap でデコレータを装飾すると、1つの注意点を除いて、引数ありでも引数なしでも動作します。

このデコレータのチェックは、デコレータが受け取ることのできる引数について仮定しています (つまり、単一の呼び出し可能な引数は受け取れないということです)。 今、私たちはこれを任意のジェネレーターに適用できるようにしているので、心に留めておくか、矛盾が生じる場合は修正する必要があります。

以下はその使い方を示しています。

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7