在 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.incrementpartial 对象,而不是您的类的实例。 是的,谢谢你的呼唤。 getter 和 setter 属性确实适用于独立函数。 【参考方案1】:

无法将property 附加到单个实例。作为描述符,propertys 必须是类定义的一部分才能起作用。这意味着您无法轻松地将它们添加到您在 __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

我建议你让修饰方法的记忆相关接口只能通过类访问,而不是通过实例。如果objNone,您需要更新您的__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 属性的主要内容,如果未能解决你的问题,请参考以下文章

Python 装饰器和类方法和评估——django memoize

将 memoization 装饰器理解为闭包

Python 中的装饰器类

用于制作 python 装饰器类的精益接口

带参数的 Python 装饰器类

我可以记住一个 Python 生成器吗?