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

Posted

技术标签:

【中文标题】Python 记忆/延迟查找属性装饰器【英文标题】:Python memoising/deferred lookup property decorator 【发布时间】:2011-03-02 01:02:35 【问题描述】:

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

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

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 抱歉,以前有其他 cmets 详细介绍它们,不知道为什么它们被删除。我现在唯一能找到的是 Python 3:functools.lru_cache() 不确定是否有内置插件(至少 Python 2.7),但有 Boltons 库的 cachedproperty @guyarad 我直到现在才看到这条评论。那是一个很棒的图书馆!将其发布为答案,以便我投票。 【参考方案1】:

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

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]

【讨论】:

有人可以为内部函数推荐一个合适的名称吗?我早上起名字很糟糕... 我通常将内部函数命名为与外部函数相同的名称,并在前面加上下划线。所以“_lazyprop” - 遵循 pep 8 的“仅供内部使用”的理念。 这很好用 :) 我不知道为什么我从来没有想过在这样的嵌套函数上使用装饰器。 考虑到非数据描述符协议,这个比下面使用__get__的答案慢得多,也不优雅 提示:在@property 下方放置一个@wraps(fn),以免丢失您的文档字符串等(wraps 来自functools【参考方案2】:

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

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

【讨论】:

这花了一点时间来理解,但绝对是一个令人惊叹的答案。我喜欢函数本身如何被它计算的值替换。 为了后代:自那以后(参考1和2)的其他答案中已经提出了其他版本。似乎这是 Python web 框架中流行的一个(派生词存在于 Pyramid 和 Werkzeug 中)。 感谢您注意到 Werkzeug 有 werkzeug.utils.cached_property:werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property 我发现这种方法比所选答案快 7.6 倍。 (2.45 µs / 322 ns)See ipython notebook 注:这不会阻止分配fget @property 的方式。为了确保不变性/幂等性,您需要添加一个引发AttributeError('can\'t set attribute')__set__() 方法(或任何适合您的异常/消息,但这就是property 引发的内容)。不幸的是,这会带来几分之一微秒的性能影响,因为每次访问都会调用__get__(),而不是在第二次和后续访问时从 dict 中提取 fget 值。在我看来,保持不变性/幂等性非常值得,这是我的用例的关键,但 YMMV。【参考方案3】:

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

【讨论】:

【参考方案4】:

property 是一个类。确切地说是descriptor。只需从中派生并实现所需的行为。

class lazyproperty(property):
   ....

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

【讨论】:

【参考方案5】:

这是一个带有可选超时参数的可调用对象,在 __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

【讨论】:

【参考方案6】:

真正想要的是 Pyramid 的 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

【讨论】:

不错的一个,正是我需要的......虽然 Pyramid 可能是一个装饰器:) 的一个很大的依赖项 @detly 装饰器实现很简单,你可以自己实现,不需要pyramid依赖。 因此链接显示“源链接”:D @AnttiHaapala 我注意到了,但我想强调一下,对于那些不点击链接的人来说,它很容易实现。【参考方案7】:

到目前为止,在问题和答案中都存在术语混淆和/或概念混淆。

惰性求值只是意味着在运行时在需要值的最后可能时刻求值。 标准的@property 装饰器就是这样做的。(*) 仅在每次需要该属性的值时才评估装饰函数。 (参见关于惰性评估的***文章)

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

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

【讨论】:

我使用了“惰性”一词,因为在最初的实现中,成员是在对象初始化时从数据库中计算/检索的,我想推迟计算直到实际使用该属性在模板中。在我看来,这符合懒惰的定义。我同意,由于我的问题已经假设使用@property 的解决方案,“懒惰”在这一点上没有多大意义。 (我还认为记忆化是输入到缓存输出的映射,由于这些属性只有一个输入,什么都没有,所以映射似乎比必要的复杂。) 请注意,当我问这个问题时,人们建议作为“开箱即用”解决方案的所有装饰器也不存在。 我同意 Jason,这是一个关于缓存/记忆而不是惰性评估的问题。 @poindexter - 缓存并没有完全覆盖它;它不区分在对象初始化时查找值并将其缓存与查找值并在访问属性时对其进行缓存(这是此处的关键功能)。我应该怎么称呼它? “首次使用后缓存”装饰器? @detly Memoize。你应该叫它记忆。 en.wikipedia.org/wiki/Memoization【参考方案8】:

您可以通过从 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

【讨论】:

以上是关于Python 记忆/延迟查找属性装饰器的主要内容,如果未能解决你的问题,请参考以下文章

在 python memoization 装饰器类中设置 get/set 属性

Python - 任何人都有一个可以处理不可散列参数的记忆装饰器吗?

python描述符property函数(类)装饰器实例解析

[python] 之 装饰器

Python基础day-7[闭包,装饰器]

Python,如何添加另一个装饰器来过滤现有多装饰器的输出与python中的属性?