Python装饰器可选参数

7
from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print f.__name__  # prints 'f'
print f.__doc__   # prints 'does some math'

在这个示例代码中,我如何使用@logged(variable)
我尝试过这样:
from functools import wraps
def logged(func):
    def outer(var):
        @wraps(func)
        def with_logging(*args, **kwargs):
            print func.__name__ + " was called"
            return func(*args, **kwargs)
        return with_logging
    return outer

我希望能像这样执行:logged(func)(session_variable)
但是不起作用。 有什么想法吗?我想要能够使用@logged和@logged(var)(甚至是@logged(var1, var2))。 谢谢。
5个回答

12

这里的诀窍是,你必须反思你所得到的东西:

def logged(*setting_args, **setting_kwargs):
    no_args = False
    if len(setting_args) == 1 \
        and not setting_kwargs \
        and callable(setting_args[0]):
        # We were called without args
        func = setting_args[0]
        no_args = True

    def outer(func):
        @wraps(func)
        def with_logging(*args, **kwargs):
            print "{} was called".format(func.__name__)
            print "Setting args are: {}".format(setting_args)
            print "Setting keyword args are: {}".format(setting_kwargs)
            return func(*args, **kwargs)
        return with_logging

    if no_args:
        return outer(func)
    else:
        return outer

这将适用于以下任何内容:

# No arguments
@logged
def some_function(x):
    pass

# One or more arguments
@logged(1, 2, 3)
def some_function(x):
    pass

# One or more keyword arguments
@logged(key=1, another_key=2)
def some_function(x):
    pass

# A mix of the two
@logged(1, 2, key=3)
def some_function(x):
    pass

如果只使用一个可调用参数调用它,则会失效

# This will break.
@logged(lambda: "Just for fun")
def some_function(x):
    pass

无法区分单个可调用设置和装饰器无参数调用之间的差异。不过,如果需要,您可以通过传递垃圾关键字参数来解决这个问题:

# This gets around the above limitation
@logged(lambda: "Just for fun", ignored=True)
def some_function(x):
    pass

太好了,谢谢。这帮助我实现了一个 required_params() 装饰器。 - Jason

6
这个问题已经有超过6年的历史,并且已经有了答案。我遇到了同样的情况 - 必须更新在代码中使用了很多地方的装饰器,并想添加一个可选参数。
我通过不同的方法完成了这个任务 - 来自书籍《Python CookBook 3rd Edition》,第9章-9.6节“定义一个带有可选参数的装饰器”。它提出了问题,给出了解决方案,并以讨论结束(非常棒)。
解决方案:适用于Python 3.3+。
from functools import wraps, partial

def logged(func=None, *, var1=None, var2=None):
    if func is None:
        return partial(logged, var1=var1, var2=var2)

    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__ + " was called"
        return func(*args, **kwargs)
    return with_logging

使用上述方法,您可以执行以下操作之一:

@logged
def f(x):

@logger(var1)
def f(x):

@logger(var1, var2)
def f(x) 

说明(最好在书中查找)

要理解代码如何工作,您需要对如何将装饰器应用于函数以及它们的调用约定有一个牢固的理解。

1. 像这样的简单装饰器:

# Example use
@logged
def add(x, y):
    return x + y

调用顺序如下:
def add(x, y):
    return x + y

add = logged(add)

在这种情况下,要包装的函数只需作为logged的第一个参数传入。因此,在解决方案中,logged()的第一个参数是被包装的函数。所有其他参数都必须具有默认值。
2. 带参数的装饰器:
@logged(level=logging.CRITICAL, name='example')
def spam():
    print('Spam!')

调用顺序如下:
def spam():
    print('Spam!')

spam = logged(level=logging.CRITICAL, name='example')(spam)

上面的最后一行展示了如何调用带有参数的装饰器,即在初始调用logged()时未传递要装饰的函数spam(),因此我们在装饰器中设置可选项,即在logged定义中将func=None。所以,在第一次调用中只传递了参数。
此举进而强制使用关键字指定其他参数。此外,当传递参数时,装饰器应返回一个接受函数并对其进行包装的函数(请参见配方9.5)。为此,解决方案使用了涉及functools.partial的巧妙技巧。具体来说,它只是返回自身的部分应用版本,其中除要包装的函数之外,所有参数都被固定。

0

您尝试的代码中存在小错误。与其使用参数为func > var > *args, **kwargs的嵌套函数,正确的顺序应该是var > func > *args, **kwargs

以下是可以满足您需求的代码片段。

from functools import wraps

def logged(var=None):
    def outer(func):
        @wraps(func)
        def with_logging(*args, **kwargs):
            print func.__name__ + " was called"
            return func(*args, **kwargs)
        return with_logging
    return outer

您可以将此装饰器称为:

@logged
def func1():
    ...

或者,

@logged(xyz)
def func2():
    ...

想要了解装饰器的工作原理,请参考文章带可选参数的装饰器


0
Sean Vieira提出的一个可能的替代方案是:
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个参数作为函数传递给装饰器,但如果您希望在多个装饰器上具有此功能,可以避免代码样板。

0
def outer(var) 放到外面,即
def outer(var):
    def logged(func):
        ...

如果你的函数中使用@outer(somevar),那么这将起作用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接