매개변수와 함께 또는 매개변수 없이 사용할 수 있는 Python 데코레이터를 만드는 방법은 무엇입니까?

StackOverflow https://stackoverflow.com/questions/653368

  •  19-08-2019
  •  | 
  •  

문제

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

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

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

@redirect_output
def foo():
    ....

그게 가능할까요?

나는 출력 리디렉션 문제에 대한 다른 해결책을 찾고 있는 것이 아니라 내가 달성하고자 하는 구문의 한 예일 뿐이라는 점에 유의하십시오.

도움이 되었습니까?

해결책

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

thobe의 답변에서 알 수 있듯이 두 경우를 모두 처리하는 유일한 방법은 두 시나리오를 모두 확인하는 것입니다.가장 쉬운 방법은 단일 인수가 있고 그것이 호출 가능한지 확인하는 것입니다(참고:데코레이터가 인수를 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)

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

두 번째 경우에는 *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

다른 팁

기본값이있는 키워드 인수 (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():
    ...

예를 들어 첫 번째 인수의 유형을 사용하여 두 경우를 모두 감지해야하며, 따라서 래퍼 (매개 변수없이 사용할 때) 또는 데코레이터 (인수와 함께 사용될 때)를 반환해야합니다.

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"). 복잡하지만 사실 :-)

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

첫 번째 예에서 :

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

나는 이것이 오래된 질문이라는 것을 알고 있지만, 제안 된 기술 중 어느 것도 마음에 들지 않으므로 다른 방법을 추가하고 싶었습니다. 나는 django가 그들의 매우 깨끗한 방법을 사용하는 것을 보았습니다. login_required 데코레이터 django.contrib.auth.decorators. 당신이 볼 수 있듯이 데코레이터의 문서, 그것은 단독으로 사용할 수 있습니다 @login_required 또는 논쟁과 함께, @login_required(redirect_field_name='my_redirect_field').

그들이하는 방식은 매우 간단합니다. 그들은 a를 추가합니다 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가 여기에서 제안한 다른 기술보다 더 우아하고 이해하기 쉬운이 접근법을 발견했습니다.

여기서 몇 가지 답변이 이미 문제를 잘 다루고 있습니다. 그러나 스타일과 관련하여, 나는이 데코레이터 곤경을 사용하여 해결하는 것을 선호합니다. functools.partial, David Beazley 's에서 제안한대로 파이썬 요리 책 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.

보조 미션 목표는 기능의 출력을 바꾸는 것이 다루어집니다. 오버플로 스택 게시물.


더 깊이 다이빙하려면 9 장 (Metaprogramming)을 확인하십시오. 파이썬 요리 책 3, 자유롭게 사용할 수 있습니다 온라인으로 읽으십시오.

그 자료 중 일부는 Beazley 's Awesome YouTube 비디오에서 라이브 데모 (더 많은!)입니다. 파이썬 3 메타 프로 그램.

행복한 코딩 :)

실제로 @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

기본값으로 키워드 인수를 시도해 보셨습니까? 같은 것

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

일반적으로 Python에서 기본 인수를 줄 수 있습니다 ...

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

그래도 데코레이터와도 작동하는지 확실하지 않습니다. 나는 그것이 왜 그런지에 대한 이유를 모른다.

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...
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top