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
Ответ 7
Это моя реализация с функцией очистки всех ленивых данных: https://gist.github.com/wonderbeyond/df700e559f7114822d15
Ответ 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