Python - 缓存属性以避免未来计算

5 投票
2 回答
3695 浏览
提问于 2025-04-16 07:40

在下面的例子中,cached_attr 用来在模型实例上获取或设置一个属性,当这个属性(例子中的 related_spam)在数据库中查询比较耗费资源时。这里我使用 cached_spam 来减少查询次数。我在设置和获取值的时候加了打印语句,这样我可以进行测试。我在一个视图中测试了这个功能,通过将一个 Egg 实例传入视图,然后在视图中使用 {{ egg.cached_spam }},同时也调用了 Egg 模型上其他会调用 cached_spam 的方法。当我完成测试后,Django 开发服务器的输出显示这个属性的缓存有好几次没有命中,但也有好几次成功获取。看起来结果有点不一致。使用相同的数据时,我稍微做了一些修改(比如只改了打印语句的内容),然后刷新页面(数据完全一样),结果的命中和未命中次数却不同。这是怎么回事?是这段代码有问题,还是说存在其他严重的问题呢?

class Egg(models.Model):
    ... fields

    @property
    def related_spam(self):
        # Each time this property is called the database is queried (expected).
        return Spam.objects.filter(egg=self).all()  # Spam has foreign key to Egg.

    @property
    def cached_spam(self):
        # This should call self.related_spam the first time, and then return
        # cached results every time after that.
        return self.cached_attr('related_spam')

    def cached_attr(self, attr):
        """This method (normally attached via an abstract base class, but put
        directly on the model for this example) attempts to return a cached
        version of a requested attribute, and calls the actual attribute when
        the cached version isn't available."""
        try:
            value = getattr(self, '_p_cache_{0}'.format(attr))
            print('GETTING - {0}'.format(value))
        except AttributeError:
            value = getattr(self, attr)
            print('SETTING - {0}'.format(value))
            setattr(self, '_p_cache_{0}'.format(attr), value)
        return value

2 个回答

1

可以扩展的Http服务器是“无共享”的,也就是说你不能依赖任何东西是单例的(即只有一个实例)。如果你想共享一些状态信息,就需要连接到一个专门的服务。

Django的缓存支持非常适合你的需求。它不一定是全局单例的;如果你使用locmem://,那么它会是进程本地的,这样可能会更高效。

10

你的代码本身没有问题,问题可能不在这里,而是在你使用这段代码的方式。

首先要明白的是,模型实例没有身份。这意味着,如果你在某个地方创建了一个Egg对象,而在另一个地方又创建了一个不同的Egg对象,即使它们指向的是同一数据库中的一行数据,它们的内部状态也不会共享。所以在一个对象上调用cached_attr并不会让另一个对象的缓存也被填充。

举个例子,假设你有一个RelatedObject类,它有一个指向Egg的外键:

my_first_egg = Egg.objects.get(pk=1)
my_related_object = RelatedObject.objects.get(egg__pk=1)
my_second_egg = my_related_object.egg

在这里,my_first_eggmy_second_egg都指向数据库中主键为1的那一行,但它们不是同一个对象:

>>> my_first_egg.pk == my_second_egg.pk
True
>>> my_first_egg is my_second_egg
False

所以,在my_first_egg上填充缓存并不会在my_second_egg上填充缓存。

当然,对象在请求之间不会保持存在(除非特别设置为全局,这样做很糟糕),所以缓存也不会保持。

撰写回答