Python 制表/延迟查找属性装饰器

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

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

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 为此包含许多现成的装饰器之前,就已经提出了这个问题。我更新它只是为了纠正术语。

36609 次浏览

property是一个类。准确地说是一个 描述符。只需从它派生并实现所需的行为。

class lazyproperty(property):
....


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

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

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]

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

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

Note: The lazy_property class is a non-data descriptor, which means it is read-only. Adding a __set__ method would prevent it from working correctly.

Here's a callable that takes an optional timeout argument, in the __call__ you could also copy over the __name__, __doc__, __module__ from func's namespace:

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

There is a mix up of terms and/or confusion of concepts both in question and in answers so far.

惰性计算只是指在需要某个值的最后一刻在运行时对某个内容进行计算。The standard @property decorator does just that.(*)修饰函数仅在每次需要该属性的值时计算。(参见维基百科关于懒惰评估的文章)

(*)实际上,真正的惰性计算(比如 haskell)在 python 中很难实现(并且导致的代码远非惯用)。

Memoization is the correct term for what the asker seems to be looking for. Pure functions that do not depend on side effects for return value evaluation can be safely memoized and there is actually a decorator in functools @functools.lru_cache so no need for writing own decorators unless you need specialized behavior.

What you 真的 want is the reify(源链接!) decorator from Pyramid:

用作类方法修饰符。它的运行方式几乎与 Python 的装饰器一模一样,但是它会在第一次调用之后把它装饰的方法的结果放到实例 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

对于各种伟大的实用程序,我使用 波顿

As part of that library you have 缓存财产:

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)

You can do this nice and easily by building a class from Python native property:

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)

Value only calculated first time and after that we used our saved value

产出:

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

他们在 Python 3.8里加了你要找的东西

将类的方法转换为一个属性,该属性的值只计算一次,然后作为实例生命周期的常规属性缓存。 与 property ()类似,添加了缓存。

就像@property:

@cached_property
def a(self):
self._a = 7
return self._a

我同意杰森的观点 当我想到惰性评估时,Asyncio 立即出现在我的脑海中。 将昂贵的计算推迟到最后一分钟的可能性是懒惰计算的唯一好处。

Caching / memozition on the other hand could be beneficial but on the expense that the calculation is static and won't change with time / inputs.

对于这些类型的昂贵计算,我经常做的一个实践是使用 TTL 进行计算然后缓存。