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 включил в него множество готовых декораторов. Я обновил его только для исправления терминологии.

10 ответов

Решение

Для всех видов отличных утилит я использую болтоны.

Как часть этой библиотеки вы кэшировали свойство:

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)

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

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

Обратите внимание lazy_property class - дескриптор не данных, что означает, что это только для чтения. Добавление __set__ метод будет препятствовать тому, чтобы это работало правильно.

property это класс. Точнее дескриптор. Просто извлеките из этого и реализуйте желаемое поведение.

class lazyproperty(property):
   ....

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

Они добавили именно то, что вы ищете в python 3.8 .

Преобразуйте метод класса в свойство, значение которого вычисляется один раз, а затем кэшируется как обычный атрибут на все время жизни экземпляра. Аналогично property(), но с добавлением кэширования.

Используйте его так же, как @property :

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

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

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

Используйте в качестве метода декоратора класса. Он работает почти так же, как Python @property decorator, но он помещает результат метода, который он декорирует, в 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

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

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

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

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

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

Я согласен с user2630413 Когда я думаю о ленивом вычислении, сразу приходит на ум Asyncio. Возможность отложить дорогостоящие вычисления до последней минуты — единственное преимущество ленивых вычислений.

С другой стороны, кэширование / запоминание может быть полезным, но за счет того, что расчет является статическим и не изменится со временем / входными данными.

Практика, которую я часто применяю для дорогостоящих вычислений такого рода, заключается в том, чтобы вычислить, а затем кэшировать с TTL.

Другие вопросы по тегам