Декоратор свойств запоминания / отложенного поиска в Python


109

Недавно я просмотрел существующую базу кода, содержащую множество классов, в которых атрибуты экземпляра отражают значения, хранящиеся в базе данных. Я отредактировал многие из этих атрибутов, чтобы их поиск в базе данных был отложен, т.е. не инициализироваться в конструкторе, а только при первом чтении. Эти атрибуты не изменяются за время существования экземпляра, но они являются настоящим узким местом для вычисления в первый раз и действительно доступны только в особых случаях. Следовательно, их также можно кэшировать после того, как они были извлечены из базы данных (поэтому это соответствует определению мемоизации, где ввод просто «нет ввода»).

Я ловлю себя на том, что снова и снова набираю следующий фрагмент кода для различных атрибутов в разных классах:

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

@Bamcclur извините, раньше были другие комментарии, подробно описывающие их, не знаю, почему они были удалены. Только один я могу найти прямо сейчас это Python 3 один: functools.lru_cache().
Detly

Не уверен, что есть встроенные модули (по крайней мере, Python 2.7), но есть cachedproperty
гайарад

@guyarad Я не видел этого комментария до сих пор. Это фантастическая библиотека! Опубликуйте это как ответ, чтобы я мог проголосовать за него.
Detly

Ответы:


12

Для самых разных утилит я использую болтоны .

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

124

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

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]

1
Может ли кто-нибудь порекомендовать подходящее имя для внутренней функции? Я так не умею называть вещи по утрам ...
Майк Боерс,

2
Я обычно называю внутреннюю функцию так же, как внешнюю, с предшествующим подчеркиванием. Итак, "_lazyprop" - следует философии "только для внутреннего использования" pep 8.
spenthil

1
Это прекрасно работает :) Не знаю, почему мне никогда не приходило в голову использовать декоратор и для такой вложенной функции.
Detly

4
учитывая протокол, не связанный с дескриптором данных, он намного медленнее и менее элегантен, чем приведенный ниже ответ с использованием__get__
Ронни

1
Совет: поставьте @wraps(fn)ниже, @propertyчтобы не потерять строки документа и т. Д. ( wrapsfunctools
Взято

111

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

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


9
Чтобы понять это, потребовалось некоторое время, но это совершенно потрясающий ответ. Мне нравится, как сама функция заменяется значением, которое она вычисляет.
Пол Этертон

2
Для потомков: другие версии этого были предложены в других ответах с тех пор (ссылки 1 и 2 ). Кажется, это популярный вариант в веб-фреймворках Python (производные существуют в Pyramid и Werkzeug).
Андре Карон

1
Спасибо, что отметили, что у Werkzeug есть werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
Я обнаружил, что этот метод в 7,6 раза быстрее выбранного ответа. (2,45 мкс / 322 нс) См. Блокнот ipython
Дэйв Батлер,

1
NB: это не мешает назначение на fgetпути @propertyделает. Чтобы гарантировать неизменность / идемпотентность, вам нужно добавить __set__()метод, который вызывает AttributeError('can\'t set attribute')(или любое другое исключение / сообщение, которое вам подходит, но это то, что propertyвызывает). К сожалению, это влияет на производительность в доли микросекунды, потому что __get__()будет вызываться при каждом доступе, а не извлекать значение fget из dict при втором и последующих доступах . На мой взгляд, стоит поддерживать неизменяемость / идемпотентность, что является ключевым для моих вариантов использования, но YMMV.
scanny

4

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


3

Что вам действительно нужно, так это декоратор reify(ссылка на источник!) От Pyramid:

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

1
Хороший, делает именно то, что мне нужно ... хотя Pyramid может быть большой зависимостью для одного декоратора:)
детально

@detly Реализация декоратора проста, и вы можете реализовать ее самостоятельно, не требуя pyramidзависимости.
Питер Вуд

Следовательно, в ссылке написано «источник связан»: D
Антти Хаапала

@AnttiHaapala Я заметил, но подумал, что хочу выделить, что это просто реализовать для тех, кто не переходит по ссылке.
Питер Вуд

1

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

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

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

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


Я использовал термин «ленивый», потому что в исходной реализации член был вычислен / получен из БД во время инициализации объекта, и я хочу отложить это вычисление до тех пор, пока свойство не будет фактически использовано в шаблоне. Мне показалось, что это соответствует определению лени. Я согласен с тем, что, поскольку мой вопрос уже предполагает использование решения @property, "ленивый" не имеет большого смысла на данном этапе. (Я также думал о мемоизации как о карте входов в кэшированные выходы, и поскольку эти свойства имеют только один вход, ничего, карта казалась более сложной, чем необходимо.)
detly

Обратите внимание, что все декораторы, которые люди предлагали в качестве «нестандартных» решений, тоже не существовали, когда я спросил об этом.
Detly

Я согласен с Джейсоном, это вопрос кеширования / запоминания, а не ленивой оценки.
poindexter 01

@poindexter - Кеширование не совсем покрывает это; он не отличает поиск значения во время инициализации объекта и его кэширование от поиска значения и его кэширования при доступе к свойству (что является здесь ключевой функцией). Как мне это назвать? Декоратор "кэширование после первого использования"?
Detly

@detly Memoize. Вы должны называть это Memoize. en.wikipedia.org/wiki/Memoization
poindexter

0

Вы можете сделать это красиво и легко, создав класс из собственного свойства 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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.