Python memoising / deferred lookup属性装饰器

时间:2010-06-10 07:26:23

标签: python decorator

最近,我浏览了一个包含许多类的现有代码库,其中实例属性反映了存储在数据库中的值。我重构了很多这些属性,以便延迟数据库查找,即。不是在构造函数中初始化,而是仅在第一次读取时初始化。这些属性在实例的生命周期内不会发生变化,但它们是第一次计算的真正瓶颈,并且只在特殊情况下才真正访问过。因此,它们也可以在从数据库中检索后进行缓存(因此符合 memoisation 的定义,其中输入只是“无输入”)。

我发现自己一遍又一遍地为各种类的各种属性输入以下代码片段:

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包含了大量现成装饰器之前,人们已经提出了这个问题。我更新了它只是为了更正术语。

8 个答案:

答案 0 :(得分:123)

这是一个惰性属性装饰器的示例实现:

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]

答案 1 :(得分:106)

我为自己写了这个...用于真正的一次性计算的懒惰属性。我喜欢它,因为它避免在对象上粘贴额外的属性,并且一旦激活就不会浪费时间检查属性存在等等:

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类是non-data descriptor,这意味着它是只读的。添加__set__方法会阻止其正常工作。

答案 2 :(得分:9)

对于我正在使用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)

答案 3 :(得分:4)

这是一个带有可选超时参数的callable,在__call__中你也可以从func的命名空间复制__name____doc____module__

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

例如:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar

答案 4 :(得分:3)

property是一个班级。确切地说descriptor。只需从中衍生出来并实现所需的行为。

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')

答案 5 :(得分:2)

你真正想要的是来自金字塔的reify (source linked!)装饰者:

  

用作类方法装饰器。它的操作几乎与Python @property装饰器完全相同,但它在第一次调用后将它装饰的方法的结果放入实例dict中,有效地用实例变量替换它装饰的函数。用Python的说法,它是一个非数据描述符。以下是一个示例及其用法:

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2

答案 6 :(得分:0)

到目前为止,问题和答案中的术语和/或概念混杂在一起。

延迟评估只是意味着在需要值的最后可能时刻在运行时评估某些内容。 标准@property装饰器就是这样做的。(*)装饰函数仅在每次需要该属性的值时进行评估。 (参见关于懒惰评估的维基百科文章)

(*)实际上,在python中很难实现真正的延迟评估(比较例如haskell)(并且导致代码远非惯用)。

记忆是提问者似乎正在寻找的正确术语。可以安全地记忆不依赖于返回值评估的副作用的纯函数,并且在functools @functools.lru_cache中实际存在装饰器,因此除非您需要专门的行为,否则不需要编写自己的装饰器。

答案 7 :(得分:0)

通过从Python本机属性构建一个类,可以很容易地做到这一点:

class cached_property(property):
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, None)
        if value is None:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

我们可以像常规类属性一样使用这个属性类(它也可以支持项目分配)

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

值仅在第一次计算,之后我们使用我们保存的值

输出:

I am calculating value
My calculated value
My calculated value
2
2