パラメータの有無にかかわらず使用できるPythonデコレータを作成するにはどうすればよいですか?


88

次のいずれかのパラメータで使用できるPythonデコレータを作成したいと思います。

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

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

@redirect_output
def foo():
    ....

それは可能ですか?

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


デフォルトの外観@redirect_outputは非常に有益ではありません。それは悪い考えだと思います。最初のフォームを使用して、あなたの人生をたくさん簡素化してください。
S.Lott 2009年

興味深い質問ですが、それを見てドキュメントを調べるまでは、@ fは@f()と同じであると思っていましたが、正直に言うと、そうあるべきだと思います(提供された引数はすべてタックされます)関数の引数について)
rog

回答:


63

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

トーブの答えが言うように、両方のケースを処理する唯一の方法は、両方のシナリオをチェックすることです。最も簡単な方法は、引数が1つで、それが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で渡された情報を何らかの方法で使用する 'new'デコレータを返します。

これは問題ありませんが、作成するすべてのデコレータに対してそれを書き出す必要があるのは、かなり煩わしく、それほどきれいではない可能性があります。代わりに、デコレータを書き直さなくても自動的に変更できると便利です...しかし、それがデコレータの目的です!

次のデコレータデコレータを使用して、デコレータのデコレータを削除し、引数の有無にかかわらず使用できるようにします。

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

31

(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():
    ...

1
何を入れyour code hereますか?装飾されている関数をどのように呼び出しますか?fn(*args, **kwargs)動作しません。
lum 2012

もっと簡単な答えがあると思います。オプションの引数でデコレータを作成するクラスを作成します。デフォルトで同じ引数を使用して別の関数を作成し、デコレータクラスの新しいインスタンスを返します。:のようなものになるはずですdef f(a = 5): return MyDecorator( a = a) し、 class MyDecorator( object ): def __init__( self, a = 5 ): .... コメントで申し訳ありませんそのハード書き込み、それを私はこれを理解するための簡単な十分にあると思います
オマールベン・ハイム

17

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

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

例:

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が使用するこのアプローチは、ここで提案されている他のどの手法よりもエレガントで理解しやすいものだと思います。


ええ、私はこの方法が好きです。デコレータを呼び出すときはkwargsを使用する必要あることに注意してください。そうしないと、最初の位置引数が割り当てられfunction、デコレータが最初の位置引数を装飾された関数であるかのように呼び出そうとするため、問題が発生します。
ダスティンワイアット

12

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

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")。これは。の場合です。複雑ですが本当です:-)


9

ここでのいくつかの答えはすでにあなたの問題にうまく対処しています。ただし、スタイルに関しては、functools.partialDavidBeazleyのPythonCookbook 3で提案されているように、を使用してこのデコレータの苦境を解決することを好みます。

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。単にで装飾するオプションがあるのが好きです。

二次的なミッションの目的については、関数の出力のリダイレクトについては、このStackOverflowの投稿で説明しています。


さらに深く掘り下げたい場合は、Python Cookbook 3の第9章(メタプログラミング)を確認してください。これは、オンラインで無料で読むことができます

その資料の一部は、Beazleyの素晴らしいYouTubeビデオPython 3メタプログラミングでライブデモされています(さらに!)。

ハッピーコーディング:)


8

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)

したがって、完全にシームレスな方法ではなく、やりたいことを実行できます。

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)

2

実際、@ 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
ありがとう。これは役に立ちます!
Pragy Agarwal 2017

0

上記よりも完全な答えを与えるには:

「引数ありと引数なしの両方で使用できるデコレータを構築する方法はありますか?」

現在、Python言語には、2つの異なるユースケースを検出するための何かが欠けているため、一般的な方法はありません

しかし、はい、すでにのような他の回答で指摘bj0S、そこにある不格好な回避策受信した最初の位置引数の型と値を確認するために(および他の引数がデフォルト以外の値を持っていないかどうかを確認すること)です。ユーザーがデコレータの最初の引数としてcallableを渡さないことが保証されている場合は、この回避策を使用できます。これはクラスデコレータでも同じであることに注意してください(前の文のクラスごとに呼び出し可能に置き換えてください)。

上記を確実にするために、私はそこにかなりの調査を行い、decopatch上記のすべての戦略(および内省を含むさらに多くの戦略)の組み合わせを使用して「最もインテリジェントな回避策は何でも」を実行するという名前のライブラリを実装しましたあなたの必要に応じて。

しかし率直に言って、ここにライブラリを必要とせず、その機能をPython言語から直接取得するのが最善です。私のように、Python言語が今日の時点でこの質問にきちんと答えることができないのは残念だと思う場合は、Pythonバグトラッカーでこのアイデアをサポートすることを躊躇しないでくださいhttps://bugs.python .org / issue36553

Pythonをより良い言語にするためにあなたの助けに感謝します:)


0

これは大騒ぎせずに仕事をします:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco

0

誰もこれに言及しなかったので、特にデコレータが複雑で、それを複数のメソッド(関数)に分割したい場合に、私がよりエレガントだと思う呼び出し可能クラスを利用するソリューションもあります。このソリューションは、__new__魔法の方法を利用して、本質的に他の人が指摘したことを実行します。リターンを適切に調整するよりも、最初にデコレータがどのように使用されたかを検出します。

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

デコレータが引数とともに使用される場合、これは次のようになります。

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

引数arg1がコンストラクターに正しく渡され、装飾された関数がに渡されることがわかります。__call__

しかし、デコレータが引数なしで使用される場合、これは次のようになります。

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

この場合、装飾された関数がコンストラクターに直接渡され、の呼び出し__call__が完全に省略されていることがわかります。そのため、__new__魔法の方法でこのケースを処理するためにロジックを使用する必要があります。

__init__代わりに使用できないのはなぜ__new__ですか?理由は単純です:pythonはNone以外の値をから返すことを禁止しています__init__

警告

このアプローチには1つの副作用があります。関数のシグネチャは保持されません!


-1

デフォルト値でキーワード引数を試しましたか?何かのようなもの

def decorate_something(foo=bar, baz=quux):
    pass

-2

通常、Pythonでデフォルトの引数を指定できます...

def redirect_output(fn, output = stderr):
    # whatever

ただし、それがデコレータでも機能するかどうかはわかりません。なぜそうならないのか、私にはわかりません。


2
@dec(abc)と言うと、関数はdecに直接渡されません。dec(abc)は何かを返し、この戻り値はデコレータとして使用されます。したがって、dec(abc)は関数を返す必要があり、関数は装飾された関数をパラメーターとして渡します。(またthobesコードを参照してください)
STH

-2

vartecの答えに基づいて:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...

これは@redirect_output("somewhere.log") def foo()、質問の例のようにデコレータとして使用することはできません。
ehabkost 2011
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.