在Python中向装饰器传递默认参数

14

我正在尝试找到一种方法,将函数的默认参数传递给装饰器。我必须说我对装饰器的了解还比较浅,所以可能我没有正确地理解它,但我还没有找到任何答案。

这是我从Python functools.wraps手册页面修改过的示例。

from functools import wraps
def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
            print('Calling decorated function')
            print('args:', args)
            print('kwargs:', kwds)
            return f(*args, **kwds)
    return wrapper

@my_decorator
def example(i, j=0):
    """Docstring"""
    print('Called example function')

example(i=1)

我希望j=0也被传递,因此输出应为:
Calling decorated function
args: ()
kwargs: {'i': 1, 'j': 0}
Called example function

但我却得到

Calling decorated function
args: ()
kwargs: {'i': 1}
Called example function

2
j=0被传递了,但不在wrapper内部。如果你在example内部打印i, j,你会看到它在那里。你可以使用例如inspect.getargspec(f)来查看被装饰函数上设置的默认值,但是为什么需要在wrapper中访问默认值呢? - jonrsharpe
我知道它被传递到了 example,但是我需要将 j 传递给 wrapper,因为我需要在许多函数中进行计算。 不过一般来说使用 inspect.getargspec(f) 就可以了,谢谢。 - Mike Volk
3个回答

11

默认参数是函数签名的一部分,它们不存在于装饰器调用中。

要在包装器中访问它们,您需要从函数中获取它们,就像这个问题所示。

import inspect
from functools import wraps

def get_default_args(func):
    signature = inspect.signature(func)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
            print('Calling decorated function')
            print('args:', args)
            kwargs = get_default_args(f)
            kwargs.update(kwds)
            print('kwargs:', kwargs)
            return f(*args, **kwds)
    return wrapper

@my_decorator
def example(i, j=0):
    """Docstring"""
    print('Called example function')

example(i=1)

输出:

Calling decorated function
args: ()
kwargs: {'i': 1, 'j': 0}
Called example function

谢谢回答,好主意!对于其他人来说,值得注意的是,dict.update()将覆盖任何带有默认值的关键字参数。您需要编写自己的dict update以避免这种行为。 - do-the-thing-please
2
调用 example(1) 会返回 kwargs: {'j': 0},但缺少 i 键可能会成为一个问题。 - abulka
@abulka Lucas Wiman的回答解决了这个问题。 - Iterniam

8

获取参数和关键字参数列表有些棘手,因为您可以将位置参数传递为关键字参数,反之亦然。较新版本的Python还添加了仅限位置参数或关键字参数。

然而,inspect.signature具有一种机制可以应用默认值:调用.bind(*args, **kwargs),然后跟随.apply_defaults()。这可以给您一个字典,有效地显示函数的所有参数。在 OP 示例中,这变为:

from functools import wraps
import inspect
def my_decorator(f):
    sig = inspect.signature(f)
    @wraps(f)
    def wrapper(*args, **kwds):
        bound = sig.bind(*args, **kwds)
        bound.apply_defaults()
        print('Calling decorated function')
        print('called with:', bound.arguments)
        return f(*args, **kwds)
    return wrapper

@my_decorator
def example(i, j=0):
    """Docstring"""
    print('Called example function')

example(i=1)

这将在Python 3.9上输出以下内容:

Calling decorated function
called with: OrderedDict([('i', 1), ('j', 0)])
Called example function

1
这是唯一适用于所有情况的答案。所有依赖于 *args、**kwargs 和字典推导式的解决方案都会忽略那些没有明确传递但依赖于字典推导式的参数。 - Iterniam
这个解决方案适用于所有情况,应该被标记为答案。谢谢!这是一个非常干净的解决方案 =) - Tim Johnsen

4
您可以使用__defaults__特殊属性获取默认参数值。
def my_decorator(f):
@wraps(f)
def wrapper(*args, **kwds):
    print('def args values', f.__defaults__)
    return f(*args, **kwds)
return wrapper

参考:在https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy中查找__defaults__ 一个元组,包含具有默认值的参数的默认参数值,如果没有参数具有默认值,则为None。

1
这将返回参数的默认值元组,但不会返回参数的名称。 - user3064538

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