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

在Python中将函数的缓存值存储为函数的属性,python,python-multithreading,Python,Python Multithreading,我希望将@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])

我希望将
@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(x=2,k=3)
(同样颠倒)相同,传递给包装器的
kwargs
args
不同

将使用更健壮的解决方案。这将使用反射来了解函数的实际参数名称(定义时)。然后,您必须“填写”在
*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
):

然后您可以使用字典
签名
本身作为调用签名。我在上面显示了一些“正确性”检查,尽管您可能只是想让对函数的调用本身检测并失败。我没有处理带有
**kwargs
*args
的函数(实际使用的名称在
argspec
中给出)。我想他们可能只是在
签名
中包含
args
kwargs
键。我仍然不确定上面的功能有多强大

更好的是,使用内置的,这是你想要的

关于线程,任何时候多个线程访问同一个数组都有同样的危险。函数属性没有什么特别之处。
lru\U缓存
应该是安全的(有一个已经解决),但有一个警告:

帮助测量缓存的有效性并调整maxsize 参数,则包装的函数使用缓存_info()进行检测 函数,该函数返回一个显示命中、未命中、maxsize和 在多线程环境中,命中和未命中是 近似


我认为您的实现没有问题,只想提及
functools。lru_cache
可以实现同样的功能。是的,我现在看到了这个陷阱。谢谢。我知道实现存在。我真正的问题是,使用自定义函数属性来实现这种目的是否节省/健壮。另一个例子是,如果我想记录所有函数调用以及一些附加信息,例如调用中花费的时间等。这更像是一个关于在运行时使用函数属性收集信息的问题。@R.Matveev在Python中,您可以(并且可以安全地)在你想要的任何对象上保存你想要的任何东西。这与你问的问题完全不同。@R.Matveev关于线程,你有着与多个线程设置同一个变量时相同的陷阱,函数上的属性没有什么特别之处。事实上,我没有很好地阐述这个问题。你的最后两条评论回答了这个问题有意提出的问题。在这种情况下是否应该编辑该问题?@R.Matveev No,这对回答者所做的所有工作都是非常不尊重的(在本例中只有我)。最好提出一个新问题,该问题应该更加简洁(例如,无需提及缓存,或者您是如何实现它们的。)感谢您的询问,而不仅仅是编辑,这对新用户来说根本不是理所当然的。
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!')