将函数的缓存值存储为Python中函数的属性

问题描述

我希望有一个@cached装饰器,类似于@memoized,该装饰器将功能的缓存值存储为功能属性。像这样

def cached(fcn):
    def cached_fcn(*args,**kwargs):
        call_signature=",".join([repr(a) for a in args] +
                                [repr(kwa[0])+"="+repr(kwa[1])
                                 for kwa in sorted(kwargs.items()) ])
        if call_signature not in cached_fcn.cache:
            cached_fcn.cache[call_signature] = fcn(*args,**kwargs)
        return copy.deepcopy(cached_fcn.cache[call_signature])
    cached_fcn.__name__ = fcn.__name__
    cached_fcn.__doc__ = fcn.__doc__
    cached_fcn.__annotations__ = fcn.__annotations__    
    cached_fcn.cache = dict()
    return cached_fcn

@cached
def fib(n):
    if n in (0,1): return 1
    return fin(n-1) + fib(n-2)

假设该函数不全局访问任何内容,这样做安全吗?如果使用线程怎么办?

解决方法

有一个陷阱可能与您的实现有关。观察

def pf(*args,**kwargs):
    print(args)
    print(kwargs)

并通过

调用
pf(1,k="a")
pf(1,"a")
pf(k="a",x=1)

所有参数规范都是具有签名f(x,k)(带有或不带有默认值)的函数的有效规范-因此您真的无法知道参数的顺序,它们的名称以及对kwargs的排序在一般情况下,绝对是不够的(第二个示例为空,而args在最后一个为空且顺序相反)。默认值会使情况变得更糟,就好像定义是f(x,k=3),然后f(2,3)f(2)f(x=2) f(2,k=3)f(x=2,k=3)(也相反)相同,只是将不同的kwargsargs传递给了包装器。

更强大的解决方案将使用inspect.getargspec(your_function)。这使用反射来了解所定义的函数的实际参数名称。然后,您必须“填写”在*args**kwargs中给定的参数,并使用该参数生成您的呼叫签名:

import inspect
def f(x,k=3): pass
argspec = inspect.getargspec(f) # returns ArgSpec(args=['x','k'],varargs=None,keywords=None,defaults=(3,)) 

现在您可以(从*args**kwargs生成呼叫签名:

signature = {}
for arg,default in zip(reversed(argspec.args),reversed(argspec.defaults)):
    signature[arg] = default

set_args = set()
for arg,val in zip(argspec.args,args):
    set_args.add(arg)
    signature[arg] = val

for arg,val in kwargs.items():
    # if arg in set_args:
    #    raise TypeError(f'{arg} set both in kwargs and in args!')
    # if arg not in argspec.args:
    #    raise TypeError(f'{arg} is not a valid argument for function!')
    signature[arg] = val

# if len(signature) == len(argspec.args):
#     raise TypeError(f'Received {len(signature)} arguments but expected {len(argspec.args)} arguments!')

然后,您可以将字典signature本身用作呼叫签名。我上面显示了一些“正确性”检查,尽管您可能只想让对函数的调用本身检测到并失败。我没有使用**kwargs*args处理函数(实际使用的名称在argspec中给出)。我认为它们可能只涉及在args中具有kwargssignature键。我仍然不确定以上内容是否可靠。

更好的是,使用内置的functools.lru_cache来满足您的需求。

关于线程,当多个线程访问同一阵列时,您将面临同样的危险。函数属性没有什么特别的。 lru_cache应该是安全的(有一个bug已得到解决),但有一个警告:

帮助衡量缓存的有效性并调整maxsize 参数,包装的函数使用cache_info()进行检测 函数返回一个命名元组,显示命中,未命中,最大大小和 currsize。在多线程环境中,命中注定是 大约