programing

선택적 인수로 데코레이터 만들기

nasanasas 2020. 12. 27. 11:05
반응형

선택적 인수로 데코레이터 만들기


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

예 : 다음 장식 my_function으로 foo_register대신 적으로 만드는 decorator.

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

예 : 다음은 예상대로 작동합니다.

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

두 응용 프로그램 (하나 method.__name__는 이름을 사용 하고 다른 하나는 이름을 전달 함) 에서 제대로 작동 하려면 내부를 확인 foo_register하여 첫 번째 인수가 데코레이터인지 확인해야합니다. 그렇다면 다음을 수행해야합니다 return decorator(method_name)(대신 return decorator). 이런 종류의 "콜 러블인지 확인"은 매우 험난 해 보입니다. 이와 같은 다용도 데코레이터를 만드는 더 좋은 방법이 있습니까?

PS 나는 이미 데코레이터를 호출하도록 요구할 수 있다는 것을 알고 있지만 그것은 "솔루션"이 아닙니다. API가 자연스럽게 느껴지기를 바랍니다. 제 아내는 꾸미기를 좋아하고 그것을 망치고 싶지 않습니다.


내가 아는 가장 깨끗한 방법은 다음과 같습니다.

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는 예상대로 데코 레이팅 된 함수를 반환합니다.

데코레이터가 다음과 같은 하나 이상의 선택적 인수로 호출되는 경우 :

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

그런 다음 None 값을 가진 함수 인수와 함께 decorator가 호출되므로 예상대로 장식하는 함수가 반환됩니다.

파이썬 3

위의 데코레이터 서명 *,은 키워드 인수의 안전한 사용을 강화하기 위해 Python 3 특정 구문 으로 개선 될 수 있습니다 . 가장 바깥 쪽 함수의 서명을 다음으로 바꾸기 만하면됩니다.

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

여기와 다른 곳의 답변과 시행 착오를 통해 데코레이터가 선택적 인수를 취하도록 만드는 훨씬 쉽고 일반적인 방법이 실제로 있음을 발견했습니다. 호출 된 인수를 확인하지만 다른 방법은 없습니다.

열쇠는 데코레이터꾸미는 것 입니다.

일반 데코레이터 데코레이터 코드

다음은 데코레이터 데코레이터입니다 (이 코드는 일반적이며 선택적 arg 데코레이터가 필요한 모든 사람이 사용할 수 있습니다) .

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__if None으로 함수에 속성을 저장하십시오 .

@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 :)'

Glenn-나는 그것을해야했다. 나는 그것을하는 "마술"방법이 없다는 것이 기쁘다 고 생각한다. 나는 그것들을 싫어한다.

그래서 여기 내 대답이 있습니다 (위와는 다르지만 동일한 개념).

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...')

어때

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

향상된 일반 데코레이터 데코레이터 코드

다음은 @Nicole 의 답변 을 다음과 같은 개선 사항으로 수정 한 것입니다.

  • 선택 사항 인 kwargs는 데코레이터로 전달할 수 있습니다.
  • 장식 된 데코레이터는 바인딩 된 방법 일 수 있습니다.
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

이 오래된 스레드가 어쨌든 맨 위로 돌아 왔으므로 이제 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

이제 마법의 데코레이터가 단 한 줄이면됩니다!

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

그건 그렇고, 이것은 모든 데코레이터에서 작동합니다. 그냥 원인 @foo같은 (가깝게 가능성으로) 행동 @foo().


데코레이터 정의를 데코 레이팅하기위한 일반 데코레이터로, 데코레이터가 명시 적으로 지정되지 않은 경우 설정되는 기본 인수를 허용 함을 표현합니다.

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

여러 데코레이터에서이 기능을 원한다면 데코레이터 용 데코레이터로 코드 보일러 플레이트를 피할 수 있습니다.

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 ...

참고 : 하나의 인수를 데코레이터에 함수로 전달할 수 없다는 단점이 있습니다.

참고 2 :이 데코레이터를 개선하는 방법에 대한 팁 / 노트가있는 경우 코드 검토 ( https://codereview.stackexchange.com/questions/78829/python-decorator-for-optional-arguments-decorator)에 댓글을 달 수 있습니다.


나는이 문제로 인해 매우 짜증이 났고 결국이를 해결하기 위해 라이브러리를 작성했습니다 : decopatch .

그것은 두 가지 개발 스타일을 지원합니다 : 중첩 (파이썬 데코레이터 팩토리에서와 같이)과 플랫 (중첩 수준이 하나 적음). 다음은 플랫 모드에서 예제를 구현하는 방법입니다.

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

서명이 완전히 보존되도록 여기 대신 makefun.wraps사용 functools.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...')

자세한 내용은 설명서확인 하십시오.


상당히 간결하고 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하나 개의 매개 변수로 호출 할 수 있습니다, 하나는 다음 할 수있는 @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(): ...

다음은 선택적 인수가 콜 러블 인 경우에도 작동하는 다른 솔루션입니다.

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

다음은 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)

문제를 해결하기 위해 간단한 패키지를 만들었습니다.

설치

마스터 브랜치 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

호출 가능한 클래스를 사용하여 인수의 유형과 길이를 확인하는 것과 유사한 솔루션

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

ReferenceURL : https://stackoverflow.com/questions/3888158/making-decorators-with-optional-arguments

반응형