Python装饰器处理装饰函数的默认参数

Python decorator handles default arguments of the decorated function

我想为类方法创建一个"cache"修饰器,它在一个内部类属性中注册该方法的结果,以避免多次计算它(我不想使用在__init__中计算的简单属性,因为我不确定是否每次都计算一次)。

第一个想法是创建一个类似于以下内容的装饰器"缓存":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def cache(func):
    name ="_{:s}".format(func.__name__)
    def wrapped(obj):
        if not hasattr(obj, name) or getattr(obj, name) is None:
            print"Computing..."
            setattr(obj, name, func(obj))
        else:
            print"Already computed!"
        return getattr(obj, name)
    return wrapped

class Test:
    @cache
    def hello(self):
        return 1000 ** 5

一切正常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [121]: t = Test()

In [122]: hasattr(t, '_hello')
Out[122]: False

In [123]: t.hello()
Computing...
Out[123]: 1000000000000000

In [124]: t.hello()
Already computed!
Out[124]: 1000000000000000

In [125]: hasattr(t, '_hello')
Out[125]: True

现在让我们说我想做同样的事情,但是当方法可以用参数(关键字和/或不是)来调用时。当然,现在我们将结果存储在不同的属性中(名称是什么?…),但在字典中,其键由*args和*kwargs组成。让我们用元组来完成它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def cache(func):
    name ="_{:s}".format(func.__name__)
    def wrapped(obj, *args, **kwargs):
        if not hasattr(obj, name) or getattr(obj, name) is None:
            setattr(obj, name, {})
        o = getattr(obj, name)
        a = args + tuple(kwargs.items())
        if not a in o:
            print"Computing..."
            o[a] = func(obj, *args, **kwargs)
        else:
            print"Already computed!"
        return o[a]
    return wrapped

class Test:
    @cache
    def hello(self, *args, **kwargs):
        return 1000 * sum(args) * sum(kwargs.values())

In [137]: t = Test()

In [138]: hasattr(t, '_hello')
Out[138]: False

In [139]: t.hello()
Computing...
Out[139]: 0

In [140]: hasattr(t, '_hello')
Out[140]: True

In [141]: t.hello(3)
Computing...
Out[141]: 0

In [142]: t.hello(p=3)
Computing...
Out[142]: 0

In [143]: t.hello(4, y=23)
Computing...
Out[143]: 92000

In [144]: t._hello
Out[144]: {(): 0, (3,): 0, (4, ('y', 23)): 92000, (('p', 3),): 0}

由于方法items在不考虑字典中的顺序的情况下将字典转换为元组,因此,如果不按相同的顺序调用关键字参数,则它可以完美地工作:

1
2
3
4
5
6
7
In [146]: t.hello(2, a=23,b=34)
Computing...
Out[146]: 114000

In [147]: t.hello(2, b=34, a=23)
Already computed!
Out[147]: 114000

我的问题是:如果该方法有默认参数,那么它将不再工作:

1
2
3
4
class Test:
    @cache
    def hello(self, a=5):
        return 1000 * a

现在它不再工作了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [155]: t = Test()

In [156]: t.hello()
Computing...
Out[156]: 5000

In [157]: t.hello(a=5)
Computing...
Out[157]: 5000

In [158]: t.hello(5)
Computing...
Out[158]: 5000

In [159]: t._hello
Out[159]: {(): 5000, (5,): 5000, (('a', 5),): 5000}

结果计算3次,因为参数的给定方式不同(即使它们是"相同"参数!).

有人知道如何在装饰器内部捕获为函数给定的"默认"值吗?

谢谢你


如果您使用的是最新版本的python,那么可以使用inspect.signature获取一个Signature对象,该对象完全封装了有关函数参数的信息。然后,可以用包装器传递的参数调用它的bind方法,以获得BoundArguments对象。调用BoundArguments上的apply_defaults方法来填充任何缺少的具有默认值的参数,并检查arguments有序字典,以查看此调用的函数参数及其值的明确列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import inspect

def cache(func):
    name ="_{:s}".format(func.__name__)
    sig = inspect.signature(func)
    def wrapped(obj, *args, **kwargs):
        cache_dict = getattr(obj, name, None)
        if cache_dict is None:
            cache_dict = {}
            setattr(obj, name, cache_dict)    
        bound_args = sig.bind(obj, *args, **kwargs)
        bound_args.apply_defaults()
        cache_key = tuple(bound_args.arguments.values())
        if not cache_key in cache_dict:
            print("Computing...")
            cache_dict[cache_key] = func(obj, *args, **kwargs)
        else:
            print("Already computed!")
        return cache_dict[cache_key]
    return wrapped

请注意,我重命名了您的ao变量,以获得更有意义的名称。我还改变了在对象上设置缓存字典的方式。用这种方式调用的getattrsetattr更少!

在python 3.3中添加了inspect.signature函数和相关类型,但在python 3.5中,BoundArguments对象上的apply_defaults方法是新的。pypi上的旧版python有一个基本功能的反向端口,但它似乎还没有包括apply_defaults。我将把这作为一个问题报告给后端的Github跟踪器。


根据参数的函数结构有多复杂,可以有多种解决方案。我喜欢的解决方案是在hello中添加内部函数。如果不想更改缓存的名称,请使用外部函数具有的相同名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Test:
    def hello(self, a=5):
        @cache
        def hello(self, a):
            return 1000 * a
        return hello(self, a)

t = Test()
t.hello()
t.hello(a=5)
t.hello(5)
t._hello

Out[111]: Computing...
Already computed!
Already computed!
{(5,): 5000}

另一种方法是在decorator中添加对默认变量的检查,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def cache(func):
    name ="_{:s}".format(func.__name__)
    def wrapped(obj, *args, **kwargs):
        if not hasattr(obj, name) or getattr(obj, name) is None:
            setattr(obj, name, {})
        o = getattr(obj, name)
        a = args + tuple(kwargs.items())
        if func.func_defaults: # checking if func have default variable
            for k in kwargs.keys():
                if k in func.func_code.co_varnames and kwargs[k] == func.func_defaults[0]:
                    a = ()
            if args:
                if args[0] == func.func_defaults[0]:
                    a = ()
        if not a in o:
            print"Computing..."
            o[a] = func(obj, *args, **kwargs)
        else:
            print"Already computed!"
        return o[a]
    return wrapped

class Test:
    @cache
    def hello(self, a=5):
        return 1000 * a

t = Test()
t.hello()
t.hello(a=5)
t.hello(5)
t._hello

Out[112]: Computing...
Already computed!
Already computed!
{(): 5000}

如果您有,例如2个默认变量,第一个代码(带有内部函数)仍然可以工作,而第二个代码需要在"默认变量检查规则"中进行更改。