Как динамически удалить декоратор из функции? - PullRequest
4 голосов
/ 18 марта 2019

Я хотел бы активировать или деактивировать «кэш» в каком-либо методе класса во время выполнения.

Я нашел способ активировать его примерно так:

(...)
setattr(self, "_greedy_function", my_cache_decorator(self._cache)(getattr(self, "_greedy_function")))
(...)

где self._cache - мой собственный объект кэша, в котором хранятся результаты self._greedy_function.

Работает нормально, но что теперь, если я хочу деактивировать кеш и "убрать" _greedy_function?

Я вижу возможное решение, сохраняя ссылку на _greedy_function до ее украшения, но, возможно, есть способ извлечь ее из декорированной функции, и это было бы лучше.

По запросу, вот декоратор и объект кеша, который я использую для кеширования результатов моих функций класса:

import logging
from collections import OrderedDict, namedtuple
from functools import wraps

logging.basicConfig(
    level=logging.WARNING,
    format='%(asctime)s %(name)s %(levelname)s %(message)s'
)

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize")

def lru_cache(cache):
    """
    A replacement for functools.lru_cache() build on a custom LRU Class.
    It can cache class methods.
    """
    def decorator(func):
        logger.debug("assigning cache %r to function %s" % (cache, func.__name__))
        @wraps(func)
        def wrapped_func(*args, **kwargs):
            try:
                ret = cache[args]
                logger.debug("cached value returned for function %s" % func.__name__)
                return ret
            except KeyError:
                try:
                    ret = func(*args, **kwargs)
                except:
                    raise
                else:
                    logger.debug("cache updated for function %s" % func.__name__)
                    cache[args] = ret
                    return ret
        return wrapped_func
    return decorator

class LRU(OrderedDict):
    """
    Custom implementation of a LRU cache, build on top of an Ordered dict.
    """
    __slots__ = "_hits", "_misses", "_maxsize"

    def __new__(cls, maxsize=128):
        if maxsize is None:
            return None
        return super().__new__(cls, maxsize=maxsize)

    def __init__(self, maxsize=128, *args, **kwargs):
        self.maxsize = maxsize
        self._hits = 0
        self._misses = 0
        super().__init__(*args, **kwargs)

    def __getitem__(self, key):
        try:
            value = super().__getitem__(key)
        except KeyError:
            self._misses += 1
            raise
        else:
            self.move_to_end(key)
            self._hits += 1
            return value

    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        if len(self) > self._maxsize:
            oldest, = next(iter(self))
            del self[oldest]

    def __delitem__(self, key):
        try:
            super().__delitem__((key,))
        except KeyError:
            pass

    def __repr__(self):
        return "<%s object at %s: %s>" % (self.__class__.__name__, hex(id(self)), self.cache_info())

    def cache_info(self):
        return CacheInfo(self._hits, self._misses, self._maxsize, len(self))

    def clear(self):
        super().clear()
        self._hits, self._misses = 0, 0

    @property
    def maxsize(self):
        return self._maxsize

    @maxsize.setter
    def maxsize(self, maxsize):
        if not isinstance(maxsize, int):
            raise TypeError
        elif maxsize < 2:
            raise ValueError
        elif maxsize & (maxsize - 1) != 0:
            logger.warning("LRU feature performs best when maxsize is a power-of-two, maybe.")
        while maxsize < len(self):
            oldest, = next(iter(self))
            print(oldest)
            del self[oldest]
        self._maxsize = maxsize

Редактировать: Я обновил свой код, используя атрибут __wrapped__, предложенный в комментариях, и он работает нормально! Все это здесь: https://gist.github.com/fbparis/b3ddd5673b603b42c880974b23db7cda (метод kik.set_cache () ...)

Ответы [ 2 ]

4 голосов
/ 19 марта 2019

Вы сделали вещи слишком сложными. Декоратор может быть просто удален с помощью del self._greedy_function. Атрибут __wrapped__ не нужен.

Вот минимальная реализация методов set_cache и unset_cache:

class LRU(OrderedDict):
    def __init__(self, maxsize=128, *args, **kwargs):
        # ...
        self._cache = dict()
        super().__init__(*args, **kwargs)

    def _greedy_function(self):
        time.sleep(1)
        return time.time()

    def set_cache(self):
        self._greedy_function = lru_cache(self._cache)(getattr(self, "_greedy_function"))

    def unset_cache(self):
        del self._greedy_function

Используя ваш декоратор lru_cache, вот результаты

o = LRU()
o.set_cache()
print('First call', o._greedy_function())
print('Second call',o._greedy_function()) # Here it prints out the cached value
o.unset_cache()
print('Third call', o._greedy_function()) # The cache is not used

Выходы

First call 1552966668.735025
Second call 1552966668.735025
Third call 1552966669.7354007
2 голосов
/ 19 марта 2019

Современные версии functools.wraps устанавливают исходную функцию в качестве атрибута __wrapped__ на создаваемые ими оболочки. (Можно искать через __closure__ вложенные функции, обычно используемые для этой цели, но можно использовать и другие типы.) Разумно ожидать, что любая оболочка будет следовать этому соглашению.

Альтернативой может быть постоянная оболочка, которой можно управлять с помощью флага , чтобы ее можно было включать и отключать, не удаляя и не восстанавливая ее. Преимущество заключается в том, что оболочка может сохранять свое состояние (здесь кэшированные значения). Флаг может быть отдельной переменной ( например, , другой атрибут объекта, содержащего упакованную функцию, если таковой имеется) или может быть атрибутом самой оболочки.

...