Python memoising/отложенный искатель свойств поиска

Недавно я просмотрел существующую базу кода, содержащую много классов, где атрибуты экземпляра отражают значения, хранящиеся в базе данных. Я отредактировал много этих атрибутов, чтобы их запросы к базам данных были отложены, т.е. не инициализируется в конструкторе, а только после первого чтения. Эти атрибуты не меняются в течение всего жизненного цикла экземпляра, но они являются реальным узким местом для вычисления этого первого времени и доступны только для особых случаев. Следовательно, они также могут быть кэшированы после того, как они были извлечены из базы данных (это, таким образом, соответствует определению 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 включил много готовых декораторов для этого. Я обновил его только для правильной терминологии.

Ответ 1

Для всех видов больших утилит я использую 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 essentially unrelated value)

Ответ 2

Вот пример реализации ленивого декоратора свойств:

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__
    @property
    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]

Ответ 3

Я написал это для себя... Используется для истинных одноразовых рассчитанных ленивых свойств. Мне это нравится, потому что это позволяет избежать наложения дополнительных атрибутов на объекты, и после активации не тратит время на проверку наличия атрибута и т.д.:

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
        self.func_name = fget.__name__

    def __get__(self, obj, cls):
        if obj is None:
            return None
        value = self.fget(obj)
        setattr(obj, self.func_name, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

Ответ 4

property - это класс. A descriptor, если быть точным. Просто выведите из него и выполните желаемое поведение.

class lazyproperty(property):
   ....

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

Ответ 5

Здесь вызываемый, который принимает необязательный аргумент таймаута, в __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

Ответ 6

Что вы действительно хотите, это reify (источник связан!) decorator из Pyramid:

Использовать в качестве декоратора класса. Он работает почти так же, как и декодер 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

Ответ 8

Существует смешанная терминология и/или путаница понятий как в вопросе, так и в ответах до сих пор.

Ленивая оценка просто означает, что что-то оценивается во время выполнения в последний возможный момент, когда требуется значение. Стандартный декодер @property делает именно это. (*) Убранная функция оценивается только и каждый раз, когда вам нужно значение этого свойства. (см. статью wikipedia об ленивой оценке)

(*) На самом деле истинной ленивой оценкой (сравните, например, haskell) очень сложно достичь в python (и приводит к коду, который далек от идиоматического).

Запоминание - это правильный термин, который, по-видимому, ищет искатель. Чистые функции, которые не зависят от побочных эффектов для оценки возвращаемого значения, могут быть безопасно сохранены в памяти и на самом деле есть декоратор в functools @functools.lru_cache, поэтому нет необходимости писать собственные декораторы, если вам не требуется специализированное поведение.

Ответ 9

Вы можете сделать это легко и просто, построив класс из собственного свойства 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