Python备注/延迟查找属性装饰器


109

最近,我浏览了现有的代码库,其中包含许多类,其中实例属性反映了存储在数据库中的值。我已经重构了许多这些属性,以便推迟它们的数据库查找。不会在构造函数中初始化,而只能在初次阅读时进行初始化。这些属性在实例的生存期内不会更改,但是它们是第一次计算该瓶颈,并且仅在特殊情况下才真正访问。因此,它们也可以在从数据库中检索出来之后进行缓存(因此,它适合于记忆的定义,其中输入只是“无输入”)。

我发现自己一遍又一遍地输入以下代码段,以获取各个类中的各个属性:

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


我使用的是python 2.7,对此我看不到任何有关现成的装饰器的信息。您能否提供问题中提到的现成装饰器的链接?
Bamcclur

@Bamcclur对不起,以前还有其他评论详细介绍了它们,但不确定为什么将其删除。我现在唯一能找到的是Python 3 :functools.lru_cache()
2016年

不确定是否有内置插件(至少Python 2.7),但是有Boltons库的缓存属性
Guyarad

@guyarad直到现在我才看到此评论。那是一个很棒的图书馆!将其发布为答案,这样我就可以投票赞成。
熟练地

Answers:


12

对于各种强大的工具,我都使用bolton

作为该库的一部分,您具有cached属性

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)

124

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

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
有人可以为内部功能推荐合适的名称吗?我很无法在早上命名...
Mike Boers 2010年

2
我通常将内部函数命名为与外部函数相同,并带有下划线。因此,“ _ lazyprop”-遵循pep 8的“仅供内部使用”的哲学。
6

1
这很好用:)我不知道为什么在这样的嵌套函数上使用装饰器也从来没有发生过。
2010年

4
给定非数据描述符协议,此协议比以下使用以下方法的答案要慢得多,也不太美观__get__
Ronny

1
提示:在@wraps(fn)下面放一个@property不要松开您的文档字符串等。(wraps来自functools
letmaik 2014年

111

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

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__方法会阻止其正常工作。


9
这花了一些时间才能理解,但这绝对是一个惊人的答案。我喜欢如何将函数本身替换为其计算的值。
Paul Etherton 2013年

2
为了后代:自此以来,其他答案中已经提出了其他版本(参考12)。似乎这是Python Web框架中流行的一种(派生存在于Pyramid和Werkzeug中)。
安德烈·卡隆

1
感谢您注意到Werkzeug具有werkzeug.utils.cached_property:werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira 2014年

3
我发现此方法比所选答案快7.6倍。(2.45 µs / 322 ns)请参阅ipython笔记本
Dave Butler

1
注意:这并不妨碍分配fget的方式@property一样。为确保不变性/幂等性,您需要添加__set__()引发的方法AttributeError('can\'t set attribute')(或任何适合您的异常/消息,但这就是property引发的原因)。不幸的是,这会对性能造成几分之一微秒的影响,因为__get__()将在每次访问时调用该方法,而不是从第二次及后续访问的dict中获取fget值。在我看来,保持不变性/幂等性是非常值得的,这对我的用例来说很关键,但对于YMMV来说却很重要。
scanny

4

这里有一个调用,它接受一个可选的超时参数,在__call__你也可以复制过__name____doc____module__从FUNC的命名空间:

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


3

真正想要的是Pyramid 的reify(源链接!)装饰器:

用作类方法装饰器。它的运行几乎与Python @property装饰器完全一样,但是它在第一次调用后将其装饰方法的结果放入实例字典中,从而用实例变量有效地替换了其装饰函数。用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

1
不错,确实满足了我的需求……虽然金字塔对于一个装饰者可能是一个很大的依赖:)
显然是

@detly装饰器实现很简单,您可以自己实现它,而无需pyramid依赖项。
彼得·伍德

因此,链接显示为“源链接”:D
Antti Haapala

我注意到了@AnttiHaapala,但我想强调一下,对于不遵循链接的用户来说,实现起来很简单。
彼得·伍德

1

到目前为止,在所讨论的问题和所回答的问题中都有术语和/或概念的混淆。

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

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

记忆化是询问者似乎正在寻找的正确术语。可以安全地记住不依赖于副作用来评估返回值的纯函数,并且functools中 实际上有一个装饰器,@functools.lru_cache因此除非需要特殊的行为,否则无需编写自己的装饰器。


我之所以使用“惰性”一词,是因为在原始实现中,成员是在对象初始化时从数据库中计算/检索的,并且我想将计算推迟到该属性实际在模板中使用之前。在我看来,这符合懒惰的定义。我同意,因为我的问题已经假定了使用的解决方案@property,所以“懒惰”在当时并没有多大意义。(我也将记忆看作是输入到缓存输出的映射,并且由于这些属性只有一个输入,什么也没有,因此映射似乎比必需的更为复杂。)
2016年

请注意,当我问这个问题时,人们建议的所有“开箱即用”解决方案装饰器都不存在。
熟练地

我同意Jason的观点,这是关于缓存/内存化而不是惰性评估的问题。
poindexter'Mar

@poindexter-缓存并没有完全涵盖它;它不区分在对象初始化时向上查找值和对其进行缓存,而不是在访问该属性时向上查找并对其进行缓存(这是此处的关键功能)。我应该怎么称呼它?“首次使用后缓存”装饰器?
确实

@detly记住。您应该将其称为“记忆”。 en.wikipedia.org/wiki/Memoization
poindexter

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.