with - python3 decorator




如何創建一個可以使用或不使用參數的Python裝飾器? (7)

我想創建一個可以與參數一起使用的Python裝飾器:

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

或沒有它們(例如,默認情況下將輸出重定向到stderr):

@redirect_output
def foo():
    ....

那可能嗎?

請注意,我不是在尋找重定向輸出問題的不同解決方案,它只是我想要實現的語法的一個示例。


一個python裝飾器以一種根本不同的方式被調用,具體取決於你是否給它參數。 裝飾實際上只是一個(語法限制)表達。

在你的第一個例子中:

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

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

實際上,可以輕鬆檢查@ 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

我知道這個問題很老,但有些評論是新的,雖然所有可行的解決方案基本相同,但大多數都不是很乾淨或易於閱讀。

就像thobe的回答所說,處理這兩種情況的唯一方法是檢查這兩種情況。 最簡單的方法是檢查是否有一個參數並且它是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)

在第一種情況下,您執行任何普通裝飾器所做的操作,返回傳入函數的修改或包裝版本。

在第二種情況下,你返回一個'new'裝飾器,它以某種方式使用* 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

我知道這是一個老問題,但我真的不喜歡任何提出的技術,所以我想添加另一種方法。 我看到django django.contrib.auth.decoratorslogin_required裝飾器中使用了一個非常乾淨的方法。 正如您在裝飾器的文檔中看到的那樣,它可以單獨用作@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使用的這種方法比這裡提出的任何其他技術更優雅,更容易理解。


通常你可以在Python中給出默認參數......

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

不過,不確定它是否適用於裝飾器。 我不知道為什麼不會這樣做。





decorator