Подтвердить что ты не робот

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), и заменить его на кешированное значение для всех последующих запросов?

Надеюсь, этот вопрос не слишком запутан, я пытался объяснить это так, как мог.

4b9b3361

Ответ 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__. Поэтому последующие "вызовы" - это просто поиск атрибутов, и нет необходимости в кэшах и т.д.