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

8 99

8 ответов:

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

как часть этой библиотеки у вас есть 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)

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

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]

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

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

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

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

class lazyproperty(property):
   ....

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

что ты действительно нужен reify (источник связан!) декоратор из пирамиды:

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

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

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

(*)самом деле истинный ленивый оценка (сравните, например, 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