web-dev-qa-db-ja.com

オプションの引数でデコレーターを作成する

_from functools import wraps

def foo_register(method_name=None):
    """Does stuff."""
    def decorator(method):
        if method_name is None:
            method.gw_method = method.__name__
        else:
            method.gw_method = method_name
        @wraps(method)
        def wrapper(*args, **kwargs):
            method(*args, **kwargs)
        return wrapper
    return decorator
_

例:以下は、decoratorにする代わりに_my_function_を_foo_register_で修飾します。

_@foo_register
def my_function():
    print('hi...')
_

例:以下は期待どおりに機能します。

_@foo_register('say_hi')
def my_function():
    print('hi...')
_

両方のアプリケーションで正しく機能させる場合(1つは_method.__name___を使用し、もう1つは名前を渡す)、_foo_register_の内部をチェックして、最初の引数がデコレータかどうかを確認する必要があります。 、私はしなければなりません:return decorator(method_name)(_return decorator_の代わりに)。この種の「呼び出し可能かどうかを確認する」は非常にハックのようです。このような多目的デコレータを作成するより良い方法はありますか?

追伸デコレータの呼び出しを要求できることはすでに知っていますが、これは「解決策」ではありません。 APIを自然に感じてほしい。私の妻は装飾が大好きで、私はそれを台無しにしたくありません。

58
orokusaki

グレン-私はそれをしなければなりませんでした。それを行う「魔法の」方法がないのは嬉しいことだと思います。私はそれらが嫌いです。

だから、ここに私自身の答えがあります(メソッド名は上記とは異なりますが、概念は同じです)。

from functools import wraps

def register_gw_method(method_or_name):
    """Cool!"""
    def decorator(method):
        if callable(method_or_name):
            method.gw_method = method.__name__
        else:
            method.gw_method = method_or_name
        @wraps(method)
        def wrapper(*args, **kwargs):
            method(*args, **kwargs)
        return wrapper
    if callable(method_or_name):
        return decorator(method_or_name)
    return decorator

使用例(どちらのバージョンも同じように機能します):

@register_gw_method
def my_function():
    print('hi...')

@register_gw_method('say_hi')
def my_function():
    print('hi...')
27
orokusaki

これを行うために私が知っている最もクリーンな方法は次のとおりです。

import functools


def decorator(original_function=None, optional_argument1=None, optional_argument2=None, ...):

    def _decorate(function):

        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            ...

        return wrapped_function

    if original_function:
        return _decorate(original_function)

    return _decorate

説明

次のようなオプションの引数なしでデコレータが呼び出された場合:

@decorator
def function ...

関数は最初の引数として渡され、decorateは期待どおりに装飾された関数を返します。

デコレータが次のような1つ以上のオプションの引数で呼び出された場合:

@decorator(optional_argument1='some value')
def function ....

次に、値Noneの関数引数を使用してデコレーターが呼び出されるため、期待どおりに修飾する関数が返されます。

Python

上記のデコレータの署名はPython 3-specific *,構文を使用して、キーワード引数を安全に使用できます。最も外側の関数のシグネチャを次のものに置き換えるだけです。

def decorator(original_function=None, *, optional_argument1=None, optional_argument2=None, ...):
51
Patbenavente

ここやその他の回答と試行錯誤の助けを借りて、デコレータがオプションの引数を取るようにする実際にはるかに簡単で一般的な方法があることがわかりました。呼び出された引数をチェックしますが、それを行う他の方法はありません。

重要なのはデコレーターを装飾するです。

ジェネリックデコレータデコレータコード

これがデコレータデコレータです(このコードは汎用であり、オプションの引数デコレータを必要とするすべての人が使用できます)

def optional_arg_decorator(fn):
    def wrapped_decorator(*args):
        if len(args) == 1 and callable(args[0]):
            return fn(args[0])

        else:
            def real_decorator(decoratee):
                return fn(decoratee, *args)

            return real_decorator

    return wrapped_decorator

使用法

使い方は次のように簡単です:

  1. 通常のようにデコレータを作成します。
  2. 最初のターゲット関数引数の後に、オプションの引数を追加します。
  3. optional_arg_decoratorでデコレータを飾ります

例:

@optional_arg_decorator
def example_decorator_with_args(fn, optional_arg = 'Default Value'):
    ...
    return fn

テストケース

あなたのユースケースについて:

したがって、あなたのケースでは、渡されたメソッド名または__name__ ifNoneを使用して関数の属性を保存するには:

@optional_arg_decorator
def register_method(fn, method_name = None):
    fn.gw_method = method_name or fn.__name__
    return fn

装飾されたメソッドを追加する

これで、使用可能なデコレータができましたargsの有無にかかわらず

@register_method('Custom Name')
def custom_name():
    pass

@register_method
def default_name():
    pass

assert custom_name.gw_method == 'Custom Name'
assert default_name.gw_method == 'default_name'

print 'Test passes :)'
37
Nicole

いかがですか

from functools import wraps, partial

def foo_register(method=None, string=None):
    if not callable(method):
        return partial(foo_register, string=method)
    method.gw_method = string or method.__name__
    @wraps(method)
    def wrapper(*args, **kwargs):
        method(*args, **kwargs)
    return wrapper
11
Oscar

拡張されたジェネリックデコレータデコレータコード

これが @ Nicoleの答え を次のように拡張したものです。

  • オプションのクワーグを装飾されたデコレータに渡すことができます
  • 装飾されたデコレータはバインドされたメソッドかもしれません
import functools

def optional_arg_decorator(fn):
    @functools.wraps(fn)
    def wrapped_decorator(*args, **kwargs):
        is_bound_method = hasattr(args[0], fn.__name__) if args else False

        if is_bound_method:
            klass = args[0]
            args = args[1:]

        # If no arguments were passed...
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            if is_bound_method:
                return fn(klass, args[0])
            else:
                return fn(args[0])

        else:
            def real_decorator(decoratee):
                if is_bound_method:
                    return fn(klass, decoratee, *args, **kwargs)
                else:
                    return fn(decoratee, *args, **kwargs)
            return real_decorator
    return wrapped_decorator
7
Ryne Everett

とにかく、この古いスレッドがトップに戻ったので、lemmeはいくつかのDecorator-ceptionをスローします。

_def magical_decorator(decorator):
    @wraps(decorator)
    def inner(*args, **kw):
        if len(args) == 1 and not kw and callable(args[0]):
            return decorator()(args[0])
        else:
            return decorator(*args, **kw)
    return inner
_

これで、魔法のデコレータが1行で完了します。

_@magical_decorator
def foo_register(...):
    # bla bla
_

ちなみに、これはどのデコレータでも機能します。 _@foo_が@foo()のように(できるだけ近くに)動作するようにします。

4
Niklas B.

デコレーター定義をデコレートするための汎用デコレーター。デコレートされたデコレーターがデフォルトの引数を受け入れることを表現します。デフォルトの引数は、明示的に何も指定されていない場合に設定されます。

from functools import wraps

def default_arguments(*default_args, **default_kwargs):
  def _dwrapper(decorator):
    @wraps(decorator)
    def _fwrapper(*args, **kwargs):
      if callable(args[0]) and len(args) == 1 and not kwargs:
        return decorator(*default_args, **default_kwargs)(args[0])
      return decorator(*args, **kwargs)
    return _fwrapper
  return _dwrapper

どちらの方法でも使用できます。

from functools import lru_cache   # memoization decorator from Python 3

# apply decorator to decorator post definition
lru_cache = (default_arguments(maxsize=100)) (lru_cache)  
# could also be:
#   @default_arguments(maxsize=100)
#   class lru_cache(object):
#     def __init__(self, maxsize):
#       ...
#     def __call__(self, wrapped_function):
#       ...


@lru_cache   # this works
def fibonacci(n):
  ...

@lru_cache(200)   # this also works
def fibonacci(n):
  ...
3
K3---rnc

私はこの問題に非常に悩まされ、最終的にそれを解決するためのライブラリー decopatch を書きました。

2つの開発スタイルをサポートしています:nested(python decoratorfactories)とflat(ネストのレベルが1つ少ない)これは、フラットモードでの例の実装方法です。

from decopatch import function_decorator, DECORATED
from makefun import wraps

@function_decorator
def foo_register(method_name=None, method=DECORATED):
    if method_name is None:
        method.gw_method = method.__name__
    else:
        method.gw_method = method_name

    # create a signature-preserving wrapper
    @wraps(method)
    def wrapper(*args, **kwargs):
        method(*args, **kwargs)

    return wrapper

ここではfunctools.wrapsの代わりに makefun.wraps を使用しているため、署名が完全に保持されます(引数が無効な場合、ラッパーはまったく呼び出されません)。

decopatchは、double-flatと呼ばれる追加の開発スタイルをサポートします。これは、このようなシグニチャー保持関数ラッパーの作成専用です。あなたの例は次のように実装されます:

from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS

@function_decorator
def foo_register(method_name=None,
                 method=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
    # this is directly the wrapper
    if method_name is None:
        method.gw_method = method.__name__
    else:
        method.gw_method = method_name

    method(*f_args, **f_kwargs)

このスタイルでは、すべてのコードがmethodの呼び出しで実行されることに注意してください。これは望ましくないかもしれません-装飾時にのみ一度だけ実行したいかもしれません-このため、以前のスタイルの方が良いでしょう。

両方のスタイルが機能することを確認できます。

@foo_register
def my_function():
    print('hi...')

@foo_register('say_hi')
def my_function():
    print('hi...')

詳細は documentation を確認してください。

1
smarie

複数のデコレーターでこの機能が必要な場合は、デコレーターのコードボイラープレートを回避できます。

from functools import wraps
import inspect


def decorator_defaults(**defined_defaults):
    def decorator(f):
        args_names = inspect.getargspec(f)[0]

        def wrapper(*new_args, **new_kwargs):
            defaults = dict(defined_defaults, **new_kwargs)
            if len(new_args) == 0:
                return f(**defaults)
            Elif len(new_args) == 1 and callable(new_args[0]):
                return f(**defaults)(new_args[0])
            else:
                too_many_args = False
                if len(new_args) > len(args_names):
                    too_many_args = True
                else:
                    for i in range(len(new_args)):
                        arg = new_args[i]
                        arg_name = args_names[i]
                        defaults[arg_name] = arg
                if len(defaults) > len(args_names):
                    too_many_args = True
                if not too_many_args:
                    final_defaults = []
                    for name in args_names:
                        final_defaults.append(defaults[name])
                    return f(*final_defaults)
                if too_many_args:
                    raise TypeError("{0}() takes {1} argument(s) "
                                    "but {2} were given".
                                    format(f.__name__,
                                           len(args_names),
                                           len(defaults)))
        return wrapper
    return decorator


@decorator_defaults(start_val="-=[", end_val="]=-")
def my_text_decorator(start_val, end_val):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return "".join([f.__name__, ' ', start_val,
                            f(*args, **kwargs), end_val])
        return wrapper
    return decorator


@decorator_defaults(end_val="]=-")
def my_text_decorator2(start_val, end_val):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return "".join([f.__name__, ' ', start_val,
                            f(*args, **kwargs), end_val])
        return wrapper
    return decorator


@my_text_decorator
def func1a(value):
    return value


@my_text_decorator()
def func2a(value):
    return value


@my_text_decorator2("-=[")
def func2b(value):
    return value


@my_text_decorator(end_val=" ...")
def func3a(value):
    return value


@my_text_decorator2("-=[", end_val=" ...")
def func3b(value):
    return value


@my_text_decorator("|> ", " <|")
def func4a(value):
    return value


@my_text_decorator2("|> ", " <|")
def func4b(value):
    return value


@my_text_decorator(end_val=" ...", start_val="|> ")
def func5a(value):
    return value


@my_text_decorator2("|> ", end_val=" ...")
def func5b(value):
    return value


print(func1a('My sample text'))  # func1a -=[My sample text]=-
print(func2a('My sample text'))  # func2a -=[My sample text]=-
print(func2b('My sample text'))  # func2b -=[My sample text]=-
print(func3a('My sample text'))  # func3a -=[My sample text ...
print(func3b('My sample text'))  # func3b -=[My sample text ...
print(func4a('My sample text'))  # func4a |> My sample text <|
print(func4b('My sample text'))  # func4b |> My sample text <|
print(func5a('My sample text'))  # func5a |> My sample text ...
print(func5b('My sample text'))  # func5b |> My sample text ...

注:1つの引数を関数としてデコレータに渡すことができないという欠点があります。

注2:このデコレーターを改善する方法に関するヒント/メモがある場合は、コードレビューでコメントできます: https://codereview.stackexchange.com/questions/78829/python-decorator-for-optional-arguments-デコレータ

オプションの引数が呼び出し可能である場合にも機能する他のソリューションを次に示します。

def test_equal(func=None, optional_value=None):
    if func is not None and optional_value is not None:
        # prevent user to set func parameter manually
        raise ValueError("Don't set 'func' parameter manually")
    if optional_value is None:
        optional_value = 10  # The default value (if needed)

    def inner(function):
        def func_wrapper(*args, **kwargs):
            # do something
            return function(*args, **kwargs) == optional_value

        return func_wrapper

    if not func:
        return inner
    return inner(func)

このようにして、両方の構文が機能します。

@test_equal
def does_return_10():
    return 10

@test_equal(optional_value=20)
def does_return_20():
    return 20

# does_return_10() return True
# does_return_20() return True
0
MajorTom

これはかなり簡潔でfunctoolsを使用しない別のバリエーションです。

_def decorator(*args, **kwargs):
    def inner_decorator(fn, foo=23, bar=42, abc=None):
        '''Always passed <fn>, the function to decorate.
        # Do whatever decorating is required.
        ...
    if len(args)==1 and len(kwargs)==0 and callable(args[0]):
        return inner_decorator(args[0])
    else:
        return lambda fn: inner_decorator(fn, *args, **kwargs)
_

_inner_decorator_を1つのパラメーターのみで呼び出すことができるかどうかに応じて、_@decorator_、@decorator()@decorator(24)などを実行できます。

これは「デコレータデコレータ」に一般化できます。

_def make_inner_decorator(inner_decorator):
    def decorator(*args, **kwargs):
        if len(args)==1 and len(kwargs)==0 and callable(args[0]):
            return inner_decorator(args[0])
        else:
            return lambda fn: inner_decorator(fn, *args, **kwargs)
    return decorator

@make_inner_decorator
def my_decorator(fn, a=34, b='foo'):
    ...

@my_decorator
def foo(): ...

@my_decorator()
def foo(): ...

@my_decorator(42)
def foo(): ...
_
0
Julian Smith

呼び出し可能なクラスを使用して引数のタイプと長さをチェックするような同様のソリューション

class decor(object):

def __init__(self, *args, **kwargs):
    self.decor_args = args
    self.decor_kwargs = kwargs

def __call__(self, *call_args, **call_kwargs):

    if callable(self.decor_args[0]) and len(self.decor_args) == 1:
        func = self.decor_args[0]
        return self.__non_param__call__(func, call_args, call_kwargs)
    else:
        func = call_args[0]
        return self.__param__call__(func)


def __non_param__call__(self, func, call_args, call_kwargs):
        print "No args"
        return func(*call_args, **call_kwargs)

def __param__call__(self, func):
    def wrapper(*args, **kwargs):
        print "With Args"
        return func(*args, **kwargs)
    return wrapper



@decor(a)
def test1(a):
    print 'test' + a

@decor
def test2(b):
    print 'test' + b
0
Gary

問題を解決するための簡単なパッケージを作成しました

Installation

マスターブランチ_pip install git+https://github.com/ferrine/biwrap_最新リリース_pip install biwrap_

概観

一部のラッパーにはオプションの引数があり、@wrapper()呼び出しを避け、代わりに_@wrapper_を使用することがよくあります。

これは単純なラッパーで機能します

_import biwrap

@biwrap.biwrap
def hiwrap(fn, hi=True):
    def new(*args, **kwargs):
        if hi:
            print('hi')
        else:
            print('bye')
        return fn(*args, **kwargs)
    return new
_

定義されたラッパーは両方の方法で使用できます

_@hiwrap
def fn(n):
    print(n)
fn(1)
#> hi
#> 1

@hiwrap(hi=False)
def fn(n):
    print(n)
fn(1)
#> bye
#> 1
_

biwrapはバインドされたメソッドでも機能します

_class O:
    @hiwrap(hi=False)
    def fn(self, n):
        print(n)

O().fn(1)
#> bye
#> 1
_

クラスのメソッド/プロパティもサポートされています

_class O:
    def __init__(self, n):
        self.n = n

    @classmethod
    @hiwrap
    def fn(cls, n):
        print(n)

    @property
    @hiwrap(hi=False)
    def num(self):
        return self.n


o = O(2)
o.fn(1)
#> hi
#> 1
print(o.num)
#> bye
#> 2
_

通話などの機能もOK

_def fn(n):
    print(n)

fn = hiwrap(fn, hi=False)
fn(1)
#> bye
#> 1
_
0
Maxim Kochurov

これがpython3用に書かれた私の解決策です。関数ではなく呼び出し可能なクラスを定義するため、他のアプローチとは異なります。

class flexible_decorator:

    def __init__(self, arg="This is default"):
        self.arg = arg

    def __call__(self, func):

        def wrapper(*args, **kwargs):
            print("Calling decorated function. arg '%s'" % self.arg)
            func(*args, **kwargs)

        return wrapper

それでも明示的にデコレータを呼び出す必要があります

@flexible_decorator()
def f(foo):
    print(foo)


@flexible_decorator(arg="This is not default")
def g(bar):
    print(bar)
0
Kanthavel