如何从函数中动态删除装饰器?

Posted

技术标签:

【中文标题】如何从函数中动态删除装饰器?【英文标题】:How to dynamically remove a decorator from a function? 【发布时间】:2019-08-08 08:09:54 【问题描述】:

我想在执行期间激活或停用某个类方法中的“缓存”。

我找到了一种方法来激活它:

(...)
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

编辑:我已经使用 cmets 中建议的 __wrapped__ 属性更新了我的代码,并且运行良好!整件事都在这里:https://gist.github.com/fbparis/b3ddd5673b603b42c880974b23db7cda (kik.set_cache() 方法...)

【问题讨论】:

"也许有办法从装饰函数中检索它" 也许,不幸的是我们不知道装饰函数是什么。 @Goyo “装饰函数”是_greedy_function,由my_cache_decorator生成。所以这个问题已经很明确了。不过,如果 PO 能提供更多装饰器的上下文会更好。 lru_cache 在您的课堂上是如何使用的?声明后我没有看到对它的任何引用。 通常包装器为此目的提供__wrapped__ 属性。也就是说,我建议使用/制作一个提供开关的包装器,而不是删除和恢复包装器。 没有理由将 setattr/getattr 与标识符一起用作字符串文字。 【参考方案1】:

你把事情弄得太复杂了。装饰器可以通过del self._greedy_function 简单地删除。不需要__wrapped__ 属性。

这是set_cacheunset_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

【讨论】:

当所讨论的函数是一种方法时,这当然是一种干净的方法。 你把 _cache 和 _greedy 函数放在 LRU 类中,但我的 LRU 类不是为此而设计的,我要缓存的东西在其他类中,这就是它复杂的原因......但也许你'是对的,所以我会先检查我的代码...... @fbparis 抱歉,我没有通读您在 Github-gist 中的 976 行代码。我刚刚看了你的kik._set_cache(...),它为kik 的其他方法设置了装饰器。我的技术应该适用于那种情况,因为缓存内容基本上是不相关的。它可以位于任何地方并设置为任何内容。 @gdlmx 无论如何感谢您的回答,我想下次我会按照您的方式进行(我发布的 976 行不可读的代码只是为了提供上下文但我没想到人们阅读它:D) 常见的用例是在函数名之前使用 @decorator 的装饰器。您实际上并没有展示如何删除它。这真的很有帮助!【参考方案2】:

functools.wraps 的现代版本将原始函数作为 __wrapped__ 属性安装在它们创建的包装器上。 (可以通过 __closure__ 搜索通常用于此目的的嵌套函数,但也可以使用其他类型。)期望任何包装器都遵循此约定是合理的。

另一种方法是使用 permanent 包装器,该包装器可以由 flag 控制,这样就可以启用和禁用它,而无需删除和恢复它。这样做的好处是包装器可以保持其状态(这里是缓存的值)。该标志可以是一个单独的变量(例如,一个承载被包装函数的对象上的另一个属性,如果有的话),也可以是包装器本身的一个属性。

【讨论】:

以上是关于如何从函数中动态删除装饰器?的主要内容,如果未能解决你的问题,请参考以下文章

python_如何在类中定义装饰器

如何在 TypeScript 中使用装饰器

python之循序渐进学习装饰器

如何让 sphinx 识别装饰的 python 函数

如何从 Python 中的函数中去除装饰器

python 装饰器和property