web-dev-qa-db-ja.com

パラメータの有無にかかわらず使用できるPythonデコレータを作成する方法は?

パラメータで使用できるPythonデコレータを作成します。

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

またはそれらなし(たとえば、デフォルトで出力をstderrにリダイレクトする):

@redirect_output
def foo():
    ....

それはまったく可能ですか?

出力をリダイレクトする問題の別の解決策を探しているわけではないことに注意してください。これは、達成したい構文の例にすぎません。

68
elifiner

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

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

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を使用してデコレータを装飾できるようになりました。これらは、引数を使用してもしなくても機能します。

上記で述べましたが、ここで繰り返す必要があります。このデコレータでのチェックは、デコレータが受け取ることができる引数(つまり、呼び出し可能な単一の引数を受け取ることができない)を想定しています。現在、どのジェネレータにも適用できるようにしているので、それを覚えておくか、矛盾する場合は変更する必要があります。

以下にその使用法を示します。

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
52
bj0

デフォルト値(kquinnで推奨)を使用してキーワード引数を使用することは良い考えですが、括弧を含める必要があります。

@redirect_output()
def foo():
    ...

デコレータで括弧なしで機能するバージョンが必要な場合は、デコレータコードで両方のシナリオを考慮する必要があります。

Python 3.0を使用している場合、これにはキーワードのみの引数を使用できます。

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

Python 2.xでは、これはvarargsトリックでエミュレートできます:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

これらのバージョンのいずれでも、次のようなコードを記述できます。

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...
29
thobe

たとえば、最初の引数のタイプを使用して両方のケースを検出し、それに応じてラッパー(パラメーターなしで使用した場合)またはデコレーター(引数付きで使用した場合)を返す必要があります。

_from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator
_

@redirect_output("output.log")構文を使用する場合、_redirect_output_は単一の引数_"output.log"_で呼び出され、引数として装飾される関数を受け入れるデコレータを返す必要があります。 _@redirect_output_として使用すると、引数として装飾される関数で直接呼び出されます。

または言い換えると、_@_構文の後に式を続ける必要があります。その結果は、修飾する関数を唯一の引数として受け入れ、修飾された関数を返す関数になります。式自体を関数呼び出しにすることもできますが、これは@redirect_output("output.log")の場合です。複雑ですが、本当です:-)

12
Remy Blank

pythonデコレータは、引数を与えるかどうかに応じて、根本的に異なる方法で呼び出されます。デコレーションは実際には単なる(構文的に制限された)式です。

最初の例では:

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

関数 redirect_outputは与えられた引数で呼び出され、それはデコレータ関数を返すことが期待され、それ自体がfooを引数として呼び出され、最終的に装飾された関数を返すことが期待されます。

同等のコードは次のようになります。

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

2番目の例の同等のコードは次のようになります。

def foo():
    ....
d = redirect_output
foo = d(foo)

したがって、あなたはcan好きなことをしますが、完全にシームレスな方法ではありません:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

関数をデコレータの引数として使用する場合を除き、これは問題ありません。その場合、デコレータは引数がないと誤って想定します。この装飾が関数タイプを返さない別の装飾に適用された場合も失敗します。

別の方法は、引数がない場合でも、常にデコレータ関数が呼び出されることを要求することです。この場合、2番目の例は次のようになります。

@redirect_output()
def foo():
    ....

デコレータ関数のコードは次のようになります。

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)
8
rog

私はこれが古い質問であることを知っていますが、提案されているどの手法も本当に好きではないので、別の方法を追加したいと思いました。 Djangoは _login_required_デコレータの_Django.contrib.auth.decorators_ で本当にクリーンなメソッドを使用しています。 デコレータのドキュメント 。これは、単独で_@login_required_として、または引数@login_required(redirect_field_name='my_redirect_field')として使用できます。

彼らのやり方はとても簡単です。デコレータの引数の前にkwarg(_function=None_)を追加します。デコレータが単独で使用される場合、functionはデコレートする実際の関数になりますが、引数を指定して呼び出される場合、functionNoneになります。

例:

_from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator
_

_@custom_decorator
def test1():
    print('test1')

>>> test1()
test1
_

_@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2
_

_@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3
_

Djangoが使用するこのアプローチは、ここで提案されている他のどの手法よりもエレガントで理解しやすいものです。

6
dgel

ここでのいくつかの回答はすでに問題をうまく解決しています。ただし、スタイルに関しては、David BeazleyのPythonクックブック3で提案されているように、functools.partialを使用してこのデコレータの窮地を解決することを好みます。

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

はい、あなたはただすることができます

@decorator()
def f(*args, **kwargs):
    pass

ファンキーな回避策がないと、見た目がおかしくなり、@decoratorで単に装飾するオプションが好きです。

二次ミッションの目的に関しては、関数の出力のリダイレクトはこの Stack Overflow post で対処されます。


さらに詳しく知りたい場合は、Python Cookbook 3の第9章(メタプログラミング)をチェックしてください。無料で入手できます オンラインで読む

その素材の一部は、Beazleyの素晴らしいYouTubeビデオ Python 3 Metaprogramming でライブデモ(さらに多く!)されています。

幸せなコーディング:)

4
henrywallace

実際、@ bj0のソリューションの警告ケースは簡単に確認できます。

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

このmeta_wrapのフェイルセーフバージョンのテストケースをいくつか示します。

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600
1
Ainz Titor