with - python wrapper decorator




Декоратор для метода класса, который кэширует возвращаемое значение после первого доступа (4)

Версия этого декоратора 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

( docs.djangoproject.com/en/1.9/_modules/django/utils/functional/… ).

Как видите, он использует func.name для определения имени функции (не нужно возиться с inspect.stack) и заменяет метод своим результатом, instance.__dict__ . Таким образом, последующие «вызовы» - это просто поиск атрибутов, и нет необходимости в каких-либо кэшах и так далее.

Моя проблема и почему

Я пытаюсь написать декоратор для метода класса @cachedproperty . Я хочу, чтобы он вел себя так, чтобы при первом вызове метода метод заменялся его возвращаемым значением. Я также хочу, чтобы он вел себя как @property чтобы его не нужно было явно вызывать. По сути, он должен быть неотличим от @property за исключением того, что он быстрее, потому что он вычисляет значение только один раз, а затем сохраняет его. Моя идея состоит в том, что это не замедлит инстанцирование, как определит его в __init__ . Вот почему я хочу сделать это.

Что я пробовал

Сначала я попытался переопределить метод fget property , но он fget только для чтения.

Затем я решил, что попробую реализовать декоратор, который нужно вызывать в первый раз, но затем кэшировать значения. Это не моя конечная цель - декоратор типа свойства, который никогда не нужно вызывать, но я подумал, что это будет более простой задачей, которую нужно решить в первую очередь. Другими словами, это нерабочее решение немного более простой проблемы.

Я старался:

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

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


Вы можете использовать что-то вроде этого:

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 если вы class_id_function сущности, и свойство может возвращать разные значения для каждого из них.

Он также проверяет ключ настроек CACHING_ENABLED если вы хотите временно отключить его при выполнении тестов.

Но он не инкапсулирует стандартный декоратор property поэтому вы все равно должны вызывать его как функцию, или вы можете использовать его так (зачем ограничивать его только свойствами):

@cached
@property
def total_sales(self):
    # Some calculations here...
    pass

Также, возможно, стоит отметить, что в случае, если вы кешируете результат из-за ленивых отношений с внешним ключом, в некоторых случаях в зависимости от ваших данных было бы быстрее просто запустить агрегатную функцию при выполнении запроса select и получении всего сразу, чем посещение кеша для каждой записи в вашем наборе результатов. Поэтому используйте какой-нибудь инструмент, например django-debug-toolbar для своей среды, чтобы сравнить, что работает лучше всего в вашем сценарии.


Прежде всего, 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'
>>>

«»»


Я думаю, вам лучше использовать собственный дескриптор, поскольку именно для этого и нужны дескрипторы. Вот так:

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"

РЕДАКТИРОВАТЬ: Кстати, вам даже не нужен пользовательский дескриптор. Вы можете просто кэшировать значение внутри вашей функции свойства. Например:

@property
def test(self):
    while True:
        try:
            return self._test
        except AttributeError:
            self._test = get_initial_value()

Это все, что нужно сделать.

Тем не менее, многие считают это злоупотреблением property и неожиданным способом его использования. А неожиданность обычно означает, что вы должны сделать это другим, более явным способом. Пользовательский дескриптор CachedProperty является очень явным, поэтому по этой причине я бы предпочел его подходу на основе property , хотя он требует больше кода.





python-decorators