매개 변수를 사용하거나 사용하지 않고 사용할 수있는 Python 데코레이터를 만드는 방법은 무엇입니까?


88

매개 변수와 함께 사용할 수있는 Python 데코레이터를 만들고 싶습니다.

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

또는 그것들없이 (예를 들어 출력을 기본적으로 stderr로 리디렉션) :

@redirect_output
def foo():
    ....

그게 가능합니까?

출력 리디렉션 문제에 대한 다른 솔루션을 찾고있는 것이 아니라 달성하고자하는 구문의 예일뿐입니다.


기본 모양 @redirect_output은 현저하게 정보가 없습니다. 나는 그것이 나쁜 생각이라고 제안하고 싶습니다. 첫 번째 양식을 사용하고 삶을 많이 단순화하십시오.
S.Lott

그래도 흥미로운 질문-내가 그것을보고 문서를 살펴보기 전까지는 @f가 @f ()와 동일하다고 가정했을 것이고, 솔직히 말하면 여전히 그래야한다고 생각합니다 (제공된 모든 인수는 그냥 다루어집니다 on the function argument)
rog

답변:


63

이 질문이 오래되었다는 것을 알고 있지만 일부 의견은 새롭고 실행 가능한 모든 솔루션은 본질적으로 동일하지만 대부분은 매우 깨끗하거나 읽기 쉽지 않습니다.

thobe의 답변에서 말했듯이 두 경우를 모두 처리하는 유일한 방법은 두 시나리오를 모두 확인하는 것입니다. 가장 쉬운 방법은 단순히 단일 인수가 있고 그것이 callabe인지 확인하는 것입니다.

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)

첫 번째 경우에는 일반 데코레이터가 수행하는 작업을 수행하고 전달 된 함수의 수정되거나 래핑 된 버전을 반환합니다.

두 번째 경우에는 * 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을 사용하여 데코레이터를 데코레이터 할 수 있으며, 한 가지주의 사항과 함께 인수를 사용하거나 사용하지 않고 작동합니다.

위에서 언급했지만 여기서 반복해야합니다.이 데코레이터의 확인은 데코레이터가받을 수있는 인수 (즉, 호출 가능한 단일 인수를받을 수 없음)에 대한 가정을합니다. 현재 모든 발전기에 적용 할 수 있도록 만들고 있기 때문에 명심하거나 모순되는 경우 수정해야합니다.

다음은 그 사용법을 보여줍니다.

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

훨씬 더 간단한 대답이 있다고 생각합니다. 선택적 인수로 데코레이터가 될 클래스를 만듭니다. 기본값을 사용하여 동일한 인수로 다른 함수를 만들고 데코레이터 클래스의 새 인스턴스를 반환합니다. 다음과 같이 보일 것입니다 : def f(a = 5): return MyDecorator( a = a) 그리고 class MyDecorator( object ): def __init__( self, a = 5 ): .... 코멘트에 그것을 작성하는 것이 힘들어 미안하지만 이것이 이해하기에 충분히 간단하기를 바랍니다
Omer Ben Haim

17

나는 이것이 오래된 질문이라는 것을 알고 있지만 제안 된 기술이 정말 마음에 들지 않아 다른 방법을 추가하고 싶었습니다. django가 .NET의 login_required데코레이터에서django.contrib.auth.decorators 정말 깨끗한 방법을 사용하는 것을 보았습니다 . 데코레이터의 문서 에서 볼 수 있듯이 단독으로 @login_required또는 인수와 함께 사용할 수 있습니다 @login_required(redirect_field_name='my_redirect_field').

그들이하는 방식은 아주 간단합니다. 데코레이터 인수 앞에 kwarg( function=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일 휴식 후 그것은 당신의 장식 기능 것처럼 장식의 시도가 첫 번째 위치 인수를 호출 할 수 있기 때문이다.
Dustin Wyatt

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.partialDavid Beazley의 Python Cookbook 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.

2 차 미션 목표와 관련하여 함수의 출력을 리디렉션하는 것은이 스택 오버플로 게시물 에서 다룹니다 .


더 자세히 알고 싶다면 온라인에서 무료로 읽을 수 있는 Python Cookbook 3의 Chapter 9 (Metaprogramming)을 확인하세요 .

그 자료 중 일부는 Beazley의 멋진 YouTube 동영상 Python 3 Metaprogramming 에서 라이브 데모 (더 많은 것!)됩니다 .

해피 코딩 :)


8

파이썬 데코레이터는 인수를 제공하는지 여부에 따라 근본적으로 다른 방식으로 호출됩니다. 장식은 실제로 (구문 적으로 제한된) 표현 일뿐입니다.

첫 번째 예에서 :

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

함수 redirect_output는 주어진 인수로 호출되며, 데코레이터 함수를 반환 할 것으로 예상되며, 이는 자체적으로 foo인수로 호출되며 (마지막으로!) 최종 데코 레이팅 된 함수를 반환 할 것으로 예상됩니다.

동등한 코드는 다음과 같습니다.

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

두 번째 예제에 해당하는 코드는 다음과 같습니다.

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)

데코레이터에 대한 인수로 함수를 사용하려는 경우가 아니면 데코레이터가 인수가 없다고 잘못 가정하지 않는 한 괜찮습니다. 이 장식이 함수 유형을 반환하지 않는 다른 장식에 적용되는 경우에도 실패합니다.

대체 방법은 데코레이터 함수가 인수가없는 경우에도 항상 호출되도록 요구하는 것입니다. 이 경우 두 번째 예는 다음과 같습니다.

@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

0

위보다 더 완전한 답변을 제공하려면 :

"인수를 포함하거나 포함하지 않고 모두 사용할 수있는 데코레이터를 구축하는 방법이 있습니까?"

아니 두 개의 다른 사용 사례를 감지하는 파이썬 언어로 뭔가 빠진 현재이 있기 때문에 더 일반적인 방법이 없습니다.

그러나 s 와 같은 다른 답변에서 이미 지적했듯이 , 수신 된 첫 번째 위치 인수의 유형과 값을 확인하고 다른 인수에 기본값이 아닌 값이 없는지 확인 하는 투박한 해결 방법 이 있습니다. 사용자가 데코레이터의 첫 번째 인수로 콜 러블을 절대 전달 하지 않는다는 것이 보장되는 경우이 해결 방법을 사용할 수 있습니다. 이것은 클래스 데코레이터의 경우와 동일하다는 점에 유의하십시오 (이전 문장에서 클래스로 호출 가능 대체).bj0

위의 사항을 확실히하기 위해 저는 거기에서 꽤 많은 연구를 수행했으며 decopatch위에 언급 된 모든 전략 (그리고 자기 성찰을 포함하여 더 많은 것)의 조합을 사용하여 "가장 지능적인 해결 방법"을 수행 하는 라이브러리를 구현했습니다. 당신의 필요에.

그러나 솔직히 가장 좋은 방법은 여기에 라이브러리가 필요하지 않고 파이썬 언어에서 바로 그 기능을 얻는 것입니다. 나처럼 파이썬 언어가 오늘이 질문에 대한 깔끔한 답을 제공 할 수 없다는 것이 유감이라고 생각한다면, 주저하지 말고 파이썬 버그 추적기 에서이 아이디어를 지원하십시오 : https : //bugs.python .org / issue36553 !

파이썬을 더 나은 언어로 만드는 데 도움을 주셔서 감사합니다 :)


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__다른 사람들이 지적한 것을 본질적으로 수행하기 위해 마술 방법을 사용 합니다. 먼저 return을 적절하게 조정하는 것보다 데코레이터가 어떻게 사용되었는지 감지하십시오.

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__입니까? 이유는 간단합니다. 파이썬은 None 이외의 다른 값을 반환하는 것을 금지합니다.__init__

경고

이 approcach에는 한 가지 부작용이 있습니다. 함수 서명을 보존하지 않습니다!


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