web-dev-qa-db-ja.com

Pythonで2つのデコレータを1つに組み合わせることができますか?

Pythonで2つのデコレータを1つの新しいデコレータに結合する方法はありますか?

関数に複数のデコレータを適用するだけでよいことに気付きましたが、2つを組み合わせて新しいものにする簡単な方法があるかどうかに興味がありました。

40
Ludo

もう少し一般的:

def composed(*decs):
    def deco(f):
        for dec in reversed(decs):
            f = dec(f)
        return f
    return deco

次に

@composed(dec1, dec2)
def some(f):
    pass

と同等です

@dec1
@dec2
def some(f):
    pass
53
Jochen Ritzel

はい。デコレータの定義を参照してください ここ

このようなものが機能するはずです:

def multiple_decorators(func):
   return decorator1(decorator2(func))

@multiple_decorators
def foo(): pass
23
Thanatos

デコレータは、関数を入力として受け取り、新しい関数を返す単なる関数です。この:

@deco
def foo():
    ...

これと同等です:

def foo():
    ...

foo = deco(foo)

つまり、decorated関数(foo)が引数としてデコレータに渡され、fooがデコレータの戻り値に置き換えられます。この知識があれば、他の2つのデコレータを組み合わせたデコレータを簡単に作成できます。

def merged_decorator(func):
    return decorator2(decorator1(func))

# now both of these function definitions are equivalent:

@decorator2
@decorator1
def foo():
    ...

@merged_decorator
def foo():
    ...

デコレータが次の2つのような引数を受け入れると、少し注意が必要になります。

@deco_with_args2(bar='bar')
@deco_with_args1('baz')
def foo():
    ...

これらのデコレータがどのように実装されているのか不思議に思うかもしれません。実際には非常に簡単です:deco_with_args1およびdeco_with_args2another関数デコレータを返す関数です。引数付きのデコレータは基本的にデコレータファクトリです。これに相当するもの:

@deco_with_args('baz')
def foo():
    ...

これは:

def foo():
    ...

real_decorator = deco_with_args('baz')
foo = real_decorator(foo)

引数を受け入れてから他の2つのデコレータを適用するデコレータを作成するには、独自のデコレータファクトリを実装する必要があります。

def merged_decorator_with_args(bar, baz):
    # pass the arguments to the decorator factories and
    # obtain the actual decorators
    deco2 = deco_with_args2(bar=bar)
    deco1 = deco_with_args1(baz)

    # create a function decorator that applies the two
    # decorators we just created
    def real_decorator(func):
        return deco2(deco1(func))

    return real_decorator

このデコレータは、次のように使用できます。

@merged_decorator_with_args('bar', 'baz')
def foo():
    ...
6
Aran-Fey

デコレータが追加の引数を取らない場合は、

def compose(f, g):
    return lambda x: f(g(x))

combined_decorator = compose(decorator1, decorator2)

@combined_decorator
def f():
    pass

と同等になります

@decorator1
@decorator2
def f():
    pass
4
Sven Marnach

テストスイートで自分自身を繰り返したくない場合は、次のようにすることができます::

def apply_patches(func):
    @functools.wraps(func)
    @mock.patch('foo.settings.USE_FAKE_CONNECTION', False)
    @mock.patch('foo.settings.DATABASE_URI', 'li://foo')
    @mock.patch('foo.connection.api.Session.post', autospec=True)
    def _(*args, **kwargs):
        return func(*args, **kwargs)

    return _

これで、各関数の上にある大量のデコレータの代わりに、テストスイートでそれを使用できます。

def ChuckNorrisCase(unittest.TestCase):
    @apply_patches
    def test_chuck_pwns_none(self):
        self.assertTrue(None)
2

そして@Jochenの答えを拡張するには:

import click


def composed(*decs):
    def deco(f):
        for dec in reversed(decs):
            f = dec(f)
        return f
    return deco


def click_multi(func):
    return composed(
        click.option('--xxx', is_flag=True, help='Some X help'),
        click.option('--zzz', is_flag=True, help='Some Z help')
    )(func)


@click_multi
def some_command(**args):
    pass

この例では、複数のデコレータを含む新しいデコレータを作成できます。

0
mraxus