在 python memoization 装饰器类中设置 get/set 属性
Posted
技术标签:
【中文标题】在 python memoization 装饰器类中设置 get/set 属性【英文标题】:Setting a get/set property in a python memoization decorator class 【发布时间】:2014-07-09 08:03:24 【问题描述】:我创建了一个装饰器记忆类,我正在积极使用它来缓存我的调用。关于如何实现 python memoization 已经有很多很好的建议。
我创建的类目前使用get 和set 方法调用来设置cacheTimeOut。它们被称为getCacheTimeOut()
和setCacheTimeOut()
。虽然这是一个适当的解决方案。我希望使用 @property
和 @cacheTimeOut.setter
装饰器来使函数能够被直接调用,例如 cacheTimeOut=120
问题在于细节。我不知道如何在 __get__
方法中访问这些属性。 __get__
方法将类中定义的不同函数调用分配给functions.partial。
这是我为 Python 2.7 设计的脚本示例
import time
from functools import partial
import cPickle
class memoize(object):
def __init__(self, func):
self.func = func
self._cache =
self._timestamps =
self._cacheTimeOut = 120
self.objtype = None
def __new__(cls, *args, **kwargs):
return object.__new__(cls,*args, **kwargs)
def __get__(self, obj, objtype=None):
"""Used for object methods where decorator has been placed before methods."""
self.objtype = objtype
fn = partial(self, obj)
fn.resetCache = self.resetCache
fn.getTimeStamps = self.getTimeStamps
fn.getCache = self.getCache
fn._timestamps = self._timestamps
fn.setCacheTimeOut = self.setCacheTimeOut
fn.getCacheTimeOut = self.getCacheTimeOut
return fn
def __argsToKey(self, *args, **kwargs):
args = list(args)
for x, arg in enumerate(args): # remove instance from
if self.objtype:
if isinstance(arg, self.objtype):
args.remove(arg)
str = cPickle.dumps(args, 1)+cPickle.dumps(kwargs, 1)
return str
def __call__(self, *args, **kwargs):
"""Main calling function of decorator."""
key = self.__argsToKey(*args, **kwargs)
now = time.time() # get current time to query for key
if self._timestamps.get(key, now) > now:
return self._cache[key]
else:
value = self.func(*args, **kwargs)
self._cache[key] = value
self._timestamps[key] = now + self._cacheTimeOut
return value
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def resetCache(self):
"""Resets the cache. Currently called manually upon request."""
self._cache =
self._timestamps =
def getCacheTimeOut(self):
"""Get the cache time out used to track stale data."""
return self._cacheTimeOut
def setCacheTimeOut(self, timeOut):
"""Set the cache timeout to some other value besides 120. Requires an integer value. If you set timeOut to zero you are ignoring the cache"""
self._cacheTimeOut = timeOut
def getCache(self):
"""Returns the cache dictionary."""
return self._cache
def getTimeStamps(self):
"""Returns the encapsulated timestamp dictionary."""
return self._timestamps
@property
def cacheTimeOut(self):
"""Get cacheTimeOut."""
return self._cacheTimeOut
@cacheTimeOut.setter
def cacheTimeOut(self, timeOut):
"""Set cacheTimeOut."""
self._cacheTimeOut = timeOut
memoize
def increment(x):
increment.count+=1
print("increment.count:%d, x:%d"%(increment.count, x))
x+=1
return x
increment.count = 0 # Define the count to track whether calls to increment vs cache
class basic(object):
def __init__(self):
self.count = 0
@memoize
def increment(self, x):
self.count+=1
print("increment.count:%d, x:%d"%(increment.count, x))
x+=1
return x
def main():
print increment(3)
print increment(3)
# What I am actually doing
print increment.getCacheTimeOut() # print out default of 120
increment.setCacheTimeOut(20) # set to 20
print increment.getCacheTimeOut() # verify that is has been set to 120
# What I would like to do and currently does not work
print increment.cacheTimeOut
# Assign to property
increment.cacheTimeOut = 20
myObject = basic()
print myObject.increment(3)
print myObject.count
print myObject.increment(3)
print myObject.count
print myObject.increment(4)
print myObject.count
####### Unittest code.
import sys
import time
import unittest
from memoize import memoize
class testSampleUsages(unittest.TestCase):
# """This series of unit tests is to show the user how to apply memoize calls."""
def testSimpleUsageMemoize(self):
@memoize
def increment(var=0):
var += 1
return var
increment(3)
increment(3)
def testMethodBasedUsage(self):
"""Add the @memoize before method call."""
class myClass(object):
@memoize
def increment(self,var=0):
var += 1
return var
@memoize
def decrement(self, var=0):
var -=1
return var
myObj = myClass()
myObj.increment(3)
myObj.increment(3)
myObj.decrement(6)
myObj.decrement(6)
def testMultipleInstances(self):
@memoize
class myClass(object):
def __init__(self):
self.incrementCountCalls = 0
self.decrementCountCalls = 0
self.powCountCall = 0
# @memoize
def increment(self,var=0):
var += 1
self.incrementCountCalls+=1
return var
# @memoize
def decrement(self, var=0):
self.decrementCountCalls+=1
var -=1
return var
def pow(self, var=0):
self.powCountCall+=1
return var*var
obj1 = myClass() # Memoizing class above does not seem to work.
obj2 = myClass()
obj3 = myClass()
obj1.increment(3)
obj1.increment(3)
#obj2.increment(3)
#obj2.increment(3)
#obj3.increment(3)
#obj3.increment(3)
obj1.pow(4)
obj2.pow(4)
obj3.pow(4)
【问题讨论】:
我有点困惑。在非方法函数上使用时,您的装饰器中的属性是否有问题?看起来它应该可以正常工作。只有方法版本会有问题,因为myObject.increment
是partial
对象,而不是您的类的实例。
是的,谢谢你的呼唤。 getter 和 setter 属性确实适用于独立函数。
【参考方案1】:
无法将property
附加到单个实例。作为描述符,property
s 必须是类定义的一部分才能起作用。这意味着您无法轻松地将它们添加到您在 __get__
中创建的 partial
对象中。
现在,您可以创建自己的类来使用您添加的属性重新实现partial
的行为。但是,我怀疑限制实际上对您有利。如果将memo
应用于方法,则其状态由该类的所有实例(甚至可能是子类的实例)共享。如果您允许通过实例调整缓存细节,您可能会将用户与以下情况混淆:
obj1 = basic()
print obj1.increment.getCacheTimeout() # prints the initial value, e.g. 120
obj2 = basic()
obj2.increment.setCacheTimeOut(20) # change the timeout value via another instance
print obj1.increment.getCacheTimeout() # the value via the first instance now prints 20
我建议你让修饰方法的记忆相关接口只能通过类访问,而不是通过实例。如果obj
是None
,您需要更新您的__get__
方法以使其工作。它可以简单地返回self
:
def __get__(self, obj, objtype=None):
if obj is None:
return self
self.objtype = objtype
return partial(self, obj) # no need to attach our methods to the partial anymore
通过此更改,通过类在memo
上使用property
有效:
basic.increment.cacheTimeOut = 20 # set property of the "unbound" method basic.increment
【讨论】:
感谢您的推荐。我理解对不同实例的混淆设置值的担忧。在处理封装和实例数据时,让实例为其特定用途处理缓存是没有意义的。如果您为实例 x 重置缓存超时,那么它不会影响实例 y 的超时。在这种情况下,每个实例也会有一个单独的缓存。 我进行了您建议的更改,将装饰器移动到课堂上调用。它似乎在缓存对象的创建而不是缓存方法。我将继续将单元测试添加到我的帖子中,以便您和其他人了解上下文。 我还修改了 get 方法添加了您的更改并注释掉了原始代码。运行我提供的单元测试似乎会产生对象的缓存而不是方法的缓存。 啊,好像剪的有点多。您可能希望在返回partial
之前保留self.objtype = objtype
行。我不确定您正在使用的行为(仅基于方法参数进行缓存,忽略 self
)是否合理,但将那行放回原处将使其与以前一样工作。
功能上的改变使 memoization 装饰器成为类级缓存。该类的所有实例将共享相同的 _cache 对象和相同的 cacheTimeOut。即使在进行更改并保留 self.objtype 之后,它的行为仍然如此。我可以调用 basic.cacheTimeOut 但仍然无法调用 basic.increment.cacheTimeOut。这可能没问题,因为正如您所说,根据对象设置 cacheTimeOut 似乎没有多大意义。【参考方案2】:
实际上有一种方法可以实现这一点 - 通过使用 call-method
将装饰器重新绑定为实例对象class Helper(object):
def __init__(self, d, obj):
self.d = d
self.obj = obj
self.timeout = 0
def __call__(self, *args, **kwargs):
print self, self.timeout
return self.d.func(self.obj, *args, **kwargs)
class decorator(object):
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, clazz):
if object is not None:
obj.__dict__[self.name] = Helper(self, obj)
return obj.__dict__[self.name]
class Foo(object):
@decorator
def bar(self, args):
return args * 2
f = Foo()
g = Foo()
f.bar.timeout = 10
g.bar.timeout = 20
print f.bar(10)
print g.bar(20)
HTH
【讨论】:
谢谢你的建议,我正在调试器中解决它。看起来您建议我将超时变量移动到由我的装饰器调用的辅助类。 memoize 实现的要求之一是它同时支持方法和独立功能。当我尝试将@decorator 添加到独立函数时,会引发错误:TypeError: 'decorator' object is not callable 所以通过添加 call 方法使其可调用。然后当然不会调用描述符协议。因此,您的超时行为不仅必须在 Helper 的 call 中实现,而且还必须在装饰器类本身上实现。如果您将实际的记忆委派给一些实用方法,这应该不会太难。以上是关于在 python memoization 装饰器类中设置 get/set 属性的主要内容,如果未能解决你的问题,请参考以下文章