Python 记忆化/延迟查找属性装饰器

115 投票
10 回答
37746 浏览
提问于 2025-04-15 23:47

最近,我在处理一个已有的代码库,这里面有很多类,它们的实例属性反映了存储在数据库中的值。我对这些属性进行了重构,让它们的数据库查询延迟执行,也就是说,不在构造函数中初始化,而是在第一次读取时才进行查询。这些属性在实例的生命周期内不会改变,但第一次计算时确实很耗时,而且通常只在特定情况下才会被访问。因此,在从数据库获取这些值后,我们可以将它们缓存起来(这就符合了记忆化的定义,因为输入实际上是“没有输入”)。

我发现自己在不同的类中反复输入以下代码片段,用于处理各种属性:

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

请问在Python中有没有现成的装饰器可以做到这一点,而我却不知道?或者,有没有比较简单的方法来定义一个这样的装饰器?

我现在使用的是Python 2.5,但如果2.6的答案有显著不同,我也会觉得很有趣。

注意

这个问题是在Python添加了很多现成的装饰器之前提出的。我更新了这个问题,只是为了纠正一些术语。

10 个回答

115

我写这个是为了自己用... 用于真正的一次性计算的懒属性。我喜欢这个方法,因为它避免在对象上添加额外的属性,而且一旦激活后,就不会浪费时间去检查属性是否存在等等。

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

    def __init__(self, fget):
        self.fget = fget

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

注意:lazy_property类是一个非数据描述符,这意味着它是只读的。如果添加一个__set__方法,就会导致它无法正常工作。

129

这里有一个懒加载属性装饰器的示例实现:

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

互动示例:

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]
16

为了使用各种很棒的工具,我在用boltons这个库。

这个库里面有一个叫cachedproperty的功能:

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)

撰写回答