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.