Python 装饰器、类方法与评估 -- django memoize

4 投票
2 回答
2089 浏览
提问于 2025-04-15 15:55

我有一个可以正常工作的记忆装饰器,它使用Django的缓存系统来记住一个函数的结果,保存一段时间。我特别把它应用在一个类的方法上。

我的装饰器看起来像这样:

def memoize(prefix='mysite', timeout=300, keygenfunc=None):
    # MUST SPECIFY A KEYGENFUNC(args, kwargs) WHICH MUST RETURN A STRING
    def funcwrap(meth):

      def keymaker(*args, **kwargs):
        key = prefix + '___' + meth.func_name + '___' + keygenfunc(args, kwargs)
        return key

      def invalidate(*args, **kwargs):
        key = keymaker(*args, **kwargs)
        cache.set(key, None, 1)

      def newfunc(*args, **kwargs):
        # construct key
        key = keymaker(*args, **kwargs)

        # is in cache?
        rv = cache.get(key)

        if rv is None:
          # cache miss
          rv = meth(*args, **kwargs)
          cache.set(key, rv, timeout)

        return rv

      newfunc.invalidate = invalidate
      return newfunc
    return funcwrap

我在一个类的方法上使用这个装饰器,所以像这样:

class StorageUnit(models.Model):
  @memoize(timeout=60*180, keygenfunc=lambda x,y: str(x[0].id))
  def someBigCalculation(self):
    ...
    return result

实际的记忆过程运行得非常好!也就是说,调用

myStorageUnitInstance.someBigCalculation()

可以正确地使用缓存。好的,太棒了!

我的问题是,当我想手动清除某个特定实例的缓存时,我希望能够运行

myStorageUnitInstance.someBigCalculation.invalidate()

然而,这样做不行,因为“self”没有被传入,所以键没有生成。我得到了一个“IndexError: tuple index out of range”的错误,指向我之前提到的lambda函数。

当然,我可以成功调用:

myStorageUnitInstance.someBigCalculation.invalidate(myStorageUnitInstance)

这完全没问题。但是,当我已经在引用一个特定实例时,这样做让我觉得有点多余。我该如何让Python把这个当作一个实例方法来处理,从而正确填充“self”变量呢?

2 个回答

-1

虽然我同意AlexM的观点,但我有一些闲暇时间,觉得这个问题挺有意思的:

# from django.whereever import cache
class memoize(object):
    def __init__(self,prefix='mysite', timeout=300, keygenfunc=None):
        class memo_descriptor(object):
            def __init__(self,func):
                self.func = func
            def __get__(self,obj,klass=None):
                key = prefix + '___' + self.func.func_name + '___' + keygenfunc(obj)
                class memo(object):
                    def __call__(s,*args,**kwargs):
                        rv = cache.get(key)
                        if rv is None:
                            rv = self.func(obj,*args, **kwargs)
                            cache.set(key, rv, timeout)
                        return rv
                    def invalidate(self):
                        cache.set(key, None, 1)
                return memo()
        self.descriptor = memo_descriptor
    def __call__(self,func):
        return self.descriptor(func)

注意,我把keygenfunc的定义从(*args,**kwargs)改成了(instance),因为你在例子中是这样使用的。如果你是根据方法调用的参数来生成一个键,而不是根据对象实例,那就不可能让someBigCalculation.invalidate这个方法以你想要的方式清除缓存。

class StorageUnit(models.Model):
    @memoize(timeout=60*180, keygenfunc=lambda x: str(x.id))
    def someBigCalculation(self):
        return 'big calculation'

这段代码里有很多内容,所以它是否真的能让你的生活更轻松,这一点值得考虑。

2

描述符(Descriptors)必须总是设置在类上,而不是实例上(具体细节可以参考这份指南)。当然,在这种情况下,你甚至不是在实例上设置它,而是在另一个函数上设置(然后作为绑定方法的一个属性来获取)。我认为,想要使用你想要的语法,唯一的方法就是让funcwrap成为一个自定义类的实例(这个类必须是描述符类,也就是说,需要定义合适的__get__方法,就像函数本身那样)。然后invalidate可以成为那个类的方法(或者,也许更好的是,成为另一个自定义类的方法,这个类的实例是由前面提到的描述符类的__get__方法产生的“类似绑定方法的东西”),最终可以达到你想要的im_self(在绑定方法中是这样命名的)。

为了你所追求的小便利,付出的代价(无论是概念上还是编码上)都相当高——高到我真的不想花一两个小时来完全开发和测试它。不过,我希望我给出的指引足够清晰,如果你仍然想继续这个方向,我会很乐意帮助你,解答任何不清楚的地方或者让你感到困惑的问题。

撰写回答