2016-05-30 339 views
1

我想为类方法创建一个“缓存”装饰器,它在内部类属性中注册避免计算多次的方法的结果(并且我不想使用一个简单的属性,在__init__中计算,因为我不确定一直计算它)。Python装饰器处理装饰函数的默认参数

的第一个想法是创建一个装饰“缓存”与此类似:

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 

一切正常:

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 

现在让我们说,我想要做同样的事情,但是当方法可以用参数调用时(keyworded和/或不)。 当然,现在我们将把结果存储在不同的属性中(名字是什么?),但是在一个字典中,它的键由* args和** kwargs组成。让我们与元组做到这一点:

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变成了一个元组的字典,而不考虑在字典顺序上,它完美的作品,如果keyworded参数不被调用同一订单:

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 

这里是我的问题:如果该方法具有默认参数,那么它不工作了:

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

现在不活像k:

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次,因为参数没有以相同的方式给出(即使它们是“相同的”参数!)。

有人知道我怎么可以捕捉给装饰器内的函数的“默认”值?

谢谢

+0

你知道[functools.lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache)吗? –

+0

@Jonas Wielicki它在3.2以下的Python中不可用。他使用Python 2.x –

+0

@VadimShkaberda对,我没有注意到它是在3.2中添加的。 –

回答

1

根据参数的功能结构有多复杂,可以有各种解决方案。我更喜欢的解决方案是将内部函数添加到hello中。如果你不想改变你的缓存的名字,给它同名的外部函数有:

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} 

另一种方法是添加为您在装饰默认的变量,例如:

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个默认变量,第一个代码(具有内部函数)仍然可以工作,而第二个代码需要在“默认变量检查规则”中进行更改。

+0

感谢您的快速回答!我喜欢这两种方式,但我想我会选择第二个,这更符合我的需要! – Edouardb

1

如果您使用的是足够新的Python版本,则可以使用inspect.signature来获取完全封装有关函数参数信息的Signature对象。然后你可以调用它的bind方法,使用你的包装器传递的参数来得到一个BoundArguments对象。呼吁BoundArgumentsapply_defaults方法填补具有缺省值的任何缺少的参数,并检查arguments有序字典看参数的功能和它们的值这个呼叫的明确上市:

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这样调用!

0123.函数和关联类型是在Python 3.3中添加的,但BoundArguments对象上的apply_defaults方法在Python 3.5中是新的。有一个旧版Python版本on PyPi的基本功能的回溯,但它似乎还不包括apply_defaults。我将在后台的github tracker上报告这个问题。