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

Recently I’ve gone through an existing code base containing many classes where instance attributes reflect values stored in a database. I’ve refactored a lot of these attributes to have their database lookups be deferred, ie. not be initialised in the constructor but only upon first read. These attributes do not change over the lifetime of the instance, but they’re a real bottleneck to calculate that first time and only really accessed for special cases. Hence they can also be cached after they’ve been retrieved from the database (this therefore fits the definition of memoisation where the input is simply “no input”).

I find myself typing the following snippet of code over and over again for various attributes across various classes:

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

Is there an existing decorator to do this already in Python that I’m simply unaware of? Or, is there a reasonably simple way to define a decorator that does this?

I’m working under Python 2.5, but 2.6 answers might still be interesting if they are significantly different.

Note

This question was asked before Python included a lot of ready-made decorators for this. I have updated it only to correct terminology.


回答 0

对于各种强大的工具,我都使用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)

For all sorts of great utilities I’m using boltons.

As part of that library you have 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)

回答 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]

Here is an example implementation of a lazy property decorator:

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)

Interactive session:

>>> 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]

回答 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该类是一个非数据描述符,这意味着它是只读的。添加__set__方法会阻止其正常工作。

I wrote this one for myself… To be used for true one-time calculated lazy properties. I like it because it avoids sticking extra attributes on objects, and once activated does not waste time checking for attribute presence, etc.:

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.


回答 3

这里有一个调用,它接受一个可选的超时参数,在__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

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]

ex:

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

property是一类。准确的描述符。只需从中派生并实现所需的行为即可。

class lazyproperty(property):
   ....

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

property is a class. A descriptor to be exact. Simply derive from it and implement the desired behavior.

class lazyproperty(property):
   ....

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

回答 5

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

What you really want is the reify (source linked!) decorator from Pyramid:

Use as a class method decorator. It operates almost exactly like the Python @property decorator, but it puts the result of the method it decorates into the instance dict after the first call, effectively replacing the function it decorates with an instance variable. It is, in Python parlance, a non-data descriptor. The following is an example and its usage:

>>> 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

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

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

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

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

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

Lazy evaluation just means that something is evaluated at runtime at the last possible moment when a value is needed. The standard @property decorator does just that.(*) The decorated function is evaluated only and every time you need the value of that property. (see wikipedia article about lazy evaluation)

(*)Actually a true lazy evaluation (compare e.g. haskell) is very hard to achieve in python (and results in code which is far from idiomatic).

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.


回答 7

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

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

We can use this property class like regular class property ( It’s also support item assignment as you can see)

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

Output:

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

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。