Decorator для метода класса, который кэширует возвращаемое значение после первого доступа
Моя проблема и почему
Я пытаюсь написать декоратор для метода класса @cachedproperty
. Я хочу, чтобы он вел себя так, что, когда метод сначала вызывается, метод заменяется его возвращаемым значением. Я также хочу, чтобы он вел себя как @property
, так что он не должен быть явно вызван. В принципе, он должен быть неотличим от @property
, за исключением того, что он быстрее, потому что он только вычисляет значение один раз, а затем сохраняет его. Моя идея заключается в том, что это не замедлит создание экземпляра, как определение его в __init__
.. Поэтому я хочу это сделать.
Что я пробовал
Сначала я попытался переопределить метод fget
property
, но он доступен только для чтения.
Затем я решил, что попытаюсь реализовать декоратор, который нужно вызвать в первый раз, но затем кэширует значения. Это не моя конечная цель декоратора типа собственности, который никогда не нужно называть, но я думал, что это будет более простая проблема для решения в первую очередь. Другими словами, это нерабочее решение для немного более простой проблемы.
Я пробовал:
def cachedproperty(func):
""" Used on methods to convert them to methods that replace themselves
with their return value once they are called. """
def cache(*args):
self = args[0] # Reference to the class who owns the method
funcname = inspect.stack()[0][3] # Name of the function, so that it can be overridden.
setattr(self, funcname, func()) # Replace the function with its value
return func() # Return the result of the function
return cache
Однако это не похоже на работу. Я тестировал это с помощью:
>>> class Test:
... @cachedproperty
... def test(self):
... print "Execute"
... return "Return"
...
>>> Test.test
<unbound method Test.cache>
>>> Test.test()
но я получаю сообщение о том, как класс не передал себя методу:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method cache() must be called with Test instance as first argument (got nothing instead)
В этот момент я и мои ограниченные знания о глубоких методах Python очень смущены, и я понятия не имею, где мой код поступил неправильно или как его исправить. (Я никогда не пытался писать декоратор раньше)
Вопрос
Как я могу написать декоратор, который будет возвращать результат вызова метода класса при первом обращении (например, @property
), и заменить его на кешированное значение для всех последующих запросов?
Надеюсь, этот вопрос не слишком запутан, я пытался объяснить это так, как мог.
Ответы
Ответ 1
Прежде всего Test
должен быть создан
test = Test()
Во-вторых, нет необходимости в inspect
, потому что мы можем получить имя свойства из func.__name__
И в-третьих, мы возвращаем property(cache)
, чтобы сделать python для выполнения всей магии.
def cachedproperty(func):
" Used on methods to convert them to methods that replace themselves\
with their return value once they are called. "
def cache(*args):
self = args[0] # Reference to the class who owns the method
funcname = func.__name__
ret_value = func(self)
setattr(self, funcname, ret_value) # Replace the function with its value
return ret_value # Return the result of the function
return property(cache)
class Test:
@cachedproperty
def test(self):
print "Execute"
return "Return"
>>> test = Test()
>>> test.test
Execute
'Return'
>>> test.test
'Return'
>>>
""
Ответ 2
Если вы не возражаете против альтернативных решений, я бы рекомендовал lru_cache
например
from functools import lru_cache
class Test:
@property
@lru_cache(maxsize=None)
def calc(self):
print("Calculating")
return 1
Ожидаемый результат
In [2]: t = Test()
In [3]: t.calc
Calculating
Out[3]: 1
In [4]: t.calc
Out[4]: 1
Ответ 3
Я думаю, что вам будет лучше с пользовательским дескриптором, так как это именно то, что нужно для дескрипторов. Например:
class CachedProperty:
def __init__(self, name, get_the_value):
self.name = name
self.get_the_value = get_the_value
def __get__(self, obj, typ):
name = self.name
while True:
try:
return getattr(obj, name)
except AttributeError:
get_the_value = self.get_the_value
try:
# get_the_value can be a string which is the name of an obj method
value = getattr(obj, get_the_value)()
except AttributeError:
# or it can be another external function
value = get_the_value()
setattr(obj, name, value)
continue
break
class Mine:
cached_property = CachedProperty("_cached_property ", get_cached_property_value)
# OR:
class Mine:
cached_property = CachedProperty("_cached_property", "get_cached_property_value")
def get_cached_property_value(self):
return "the_value"
EDIT: Кстати, вам даже не нужен собственный дескриптор. Вы можете просто кэшировать значение внутри своей функции свойства. Например:
@property
def test(self):
while True:
try:
return self._test
except AttributeError:
self._test = get_initial_value()
Вот и все.
Однако многие считают это немного злоупотреблением property
и непредвиденным способом его использования. И неожиданно, как правило, вы должны делать это другим, более явным образом. Пользовательский дескриптор CachedProperty
очень явный, поэтому по этой причине я предпочел бы его подход property
, хотя для этого требуется больше кода.
Ответ 4
Вы можете использовать что-то вроде этого:
def cached(timeout=None):
def decorator(func):
def wrapper(self, *args, **kwargs):
value = None
key = '_'.join([type(self).__name__, str(self.id) if hasattr(self, 'id') else '', func.__name__])
if settings.CACHING_ENABLED:
value = cache.get(key)
if value is None:
value = func(self, *args, **kwargs)
if settings.CACHING_ENABLED:
# if timeout=None Django cache reads a global value from settings
cache.set(key, value, timeout=timeout)
return value
return wrapper
return decorator
При добавлении в словарь кэша он генерирует ключи на основе соглашения class_id_function
в случае, если вы кэшируете сущности, и свойство может возвращать другое значение для каждого из них.
Он также проверяет ключ настроек CACHING_ENABLED
, если вы хотите временно отключить его при выполнении тестов.
Но он не инкапсулирует стандартный декодер property
, поэтому вы все равно должны называть его как функцию, или можете использовать его как это (зачем ограничивать его только свойствами):
@cached
@property
def total_sales(self):
# Some calculations here...
pass
Также стоит отметить, что в случае, если вы кэшируете результат из ленивых отношений с внешним ключом, в зависимости от ваших данных бывают времена, когда было бы быстрее просто выполнить агрегатную функцию, когда вы делаете свой запрос выбора и извлекаете все на один раз, чем посещение кеша для каждой записи в вашем результирующем наборе. Поэтому используйте какой-либо инструмент, например django-debug-toolbar
для вашей структуры, чтобы сравнить то, что лучше всего работает в вашем сценарии.
Ответ 5
Версия Django этого декоратора делает именно то, что вы описываете, и прост, поэтому, кроме моего комментария, я просто скопирую его здесь:
class cached_property(object):
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
Optional ``name`` argument allows you to make cached properties of other
methods. (e.g. url = cached_property(get_absolute_url, name='url') )
"""
def __init__(self, func, name=None):
self.func = func
self.__doc__ = getattr(func, '__doc__')
self.name = name or func.__name__
def __get__(self, instance, type=None):
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
(источник).
Как вы можете видеть, он использует имя func.name для определения имени функции (нет необходимости возиться с проверкой .stack), и он заменяет метод своим результатом, изменяя instance.__dict__
. Поэтому последующие "вызовы" - это просто поиск атрибутов, и нет необходимости в кэшах и т.д.