修改冷却装饰器以适用于方法而不是函数

Posted

技术标签:

【中文标题】修改冷却装饰器以适用于方法而不是函数【英文标题】:Modifying a cooldown decorator to work for methods instead of functions 【发布时间】:2016-03-01 03:28:56 【问题描述】:

我正在尝试创建一个装饰器,该装饰器适用于对它们应用“冷却”的方法,这意味着它们不能在一定时间内被多次调用。我已经为函数创建了一个:

>>> @cooldown(5)
... def f():
...     print('f() was called')
...
>>> f()
f() was called
>>> f()  # Nothing happens when called immediately
>>> f()  # This is 5 seconds after first call
f() was called

但我需要它来支持类的方法而不是普通函数:

>>> class Test:
...    @cooldown(6)
...    def f(self, arg):
...        print(self, arg)
...
>>> t = Test()
>>> t.f(1)
<Test object at ...> 1
>>> t.f(2)
>>> t.f(5)  # Later
<Test object at ...> 5

这是我为使其适用于正常功能而创建的:

import time

class _CooldownFunc:
    def __init__(self, func, duration):
        self._func = func
        self.duration = duration
        self._start_time = 0

    @property
    def remaining(self):
        return self.duration - (time.time() - self._start_time)

    @remaining.setter
    def remaining(self, value):
        self._start_time = time.time() - (self.duration - value)

    def __call__(self, *args, **kwargs):
        if self.remaining <= 0:
            self.remaining = self.duration
            return self._func(*args, **kwargs)

    def __getattr__(self, attr):
        return self._func.__getattribute__(attr)


def cooldown(duration):
    def decorator(func):
        return _CooldownFunc(func, duration)
    return decorator

但这不适用于方法,因为它将_CooldownFunction 对象作为self 传递并完全忽略原始self。 我如何让它与方法一起工作,正确传递原始的 self 而不是 _CooldownFunction 对象?

此外,用户还需要能够即时更改剩余时间,这使得这变得更加困难(不能只使用__get__ 来返回functools.partial(self.__call__, obj) 或其他东西):

>>> class Test:
...     @cooldown(10)
...     def f(self, arg): 
...         print(self, arg)
...
>>> t = Test()
>>> t.f(5)
<Test object at ...> 5
>>> t.f.remaining = 0
>>> t.f(3)  # Almost immediately after previous call
<Test object at ...> 3

编辑:它只需要对方法起作用,而不是对方法和函数都起作用。

编辑 2: 这个设计一开始就有一个巨大的缺陷。虽然它适用于普通功能,但我希望它分别装饰每个实例。目前,如果我有两个实例t1t2 并调用t1.f(),我不能再调用t2.f(),因为冷却时间是f() 方法而不是实例。我可能可以为此使用某种字典,但在意识到这一点之后,我更加迷失了......

【问题讨论】:

请注意,装饰器的__call__ 只被调用一次,返回之后被调用的函数。您是否尝试过将您的测试封闭在一个闭包中并返回它? @Felk cooldown 函数是装饰器,而不是 _CooldownFunc 类。 cooldown 装饰器将原始函数替换为 _CooldownFunc 对象。因此,当您尝试调用原始函数时,实际上是在调用_CooldownFunc 对象的__call__,它检查冷却时间并在内部调用存储在_func 属性中的原始函数。所以是的,每次调用装饰函数时都会调用__call__。它对于正常功能非常有效,但是方法的self 参数带来了麻烦。 哦,我错过了,对不起。但话又说回来,cooldown 装饰器不应该像现在一样为每次调用 func 创建一个新的 _CooldownFunc 实例,对吧? @Felk 当然应该,_CooldownFunc 包含各个功能的持续时间和开始时间。如果我想用冷却时间装饰多个函数,我需要它们都有单独的开始时间和持续时间,因此我需要为每个函数创建一个新的 _CooldownFunc 对象。 @Felk Ups 我想我误读了你的问题。它不会为某个函数的每次调用创建一个新的_CooldownFunc 实例,而是为每个函数创建一个_CooldownFunc 的新实例。它基本上将一个函数作为输入,并用 _CooldownFunc 对象替换该函数,然后可以像原来的输入函数一样调用它。 【参考方案1】:

您可以覆盖类的__get__ 方法以使其成为描述符。 __get__ 方法将在有人从其包含对象中获取装饰方法时调用,并传递包含对象,然后您将能够将其传递给原始方法。它返回一个实现您需要的功能的对象。

def __get__(self, obj, objtype):
    return Wrapper(self, obj)

Wrapper 对象实现了__call__,以及您想要的任何属性,因此将这些实现移到该对象中。它看起来像:

class Wrapper:
    def __init__(self, cdfunc, obj):
        self.cdfunc = cdfunc
        self.obj = obj
    def __call__(self, *args, **kwargs):
        #do stuff...
        self.cdfunc._func(self.obj, *args, **kwargs)
    @property
    def remaining(self):
        #...get needed things from self.cdfunc

【讨论】:

这是一个我没有想到的非常好的方法。但是,它不允许我访问 remaining 属性,如我的问题末尾所示。这是我的计划的重要组成部分。 @MarkusMeskanen 啊,我没有注意到问题的那一部分。如果使用 getter/setter 函数而不是属性就足够了,您可以轻松添加它们(例如 inner.get_remaining = lambda: cf.remaining)。否则我不确定如何实现。 这总比没有好,但仍然不完美。我会等待并考虑我或其他人是否可以提出允许使用该属性的解决方案。 @MarkusMeskanen 我刚刚想出了一个。 我只是在追求与__get__ 类似的东西,但你更快,看起来不错,而且似乎工作!但是,如果可以同时从两个对象访问该方法,这种方法是否安全?比如,第一个访问该方法并将obj 设置为第一个实例,然后在执行过程中obj 将更改为第二个实例,第一个实例的执行将使用第二个obj 的数据错误地完成?【参考方案2】:

修复了 interjay 解决的问题,我快速重写了你的冷却装饰器,它现在适用于各种功能/方法:

class cooldown(object):
    def __init__(self, duration):
        self._duration = duration
        self._storage = self
        self._start_time = 0

    def __getRemaining(self):
        if not hasattr(self._storage, "_start_time"):
            self._storage._start_time = 0
        return self._duration - (time.time() -
                                 self._storage._start_time)

    def __setRemaining(self, value):
        self._storage._start_time = time.time() - (self._duration -
                                                   value)

    remaining = property(__getRemaining, __setRemaining)

    def __call__(self, func):
        is_method = inspect.getargspec(func).args[0] == 'self'
        def call_if(*args, **kwargs):
            if is_method :
                self._storage = args[0]
            else:
                self._storage = self
            if self.remaining <= 0:
                self.remaining = self._duration
                return func(*args, **kwargs)

        call_if.setRemaining = self.__setRemaining
        call_if.getRemaining = self.__getRemaining
        return call_if

测试:

@cooldown(2)
def foo(stuff):
    print("foo: %s" % stuff)

foo(1)
foo(2)
time.sleep(3)
foo(3)
foo.setRemaining(0)
foo(4)

class Bla(object):
    @cooldown(2)
    def bar(self, stuff):
        print("bar: %s" % stuff)

bla = Bla()
bla.bar(1)
bla.bar.setRemaining(0)
bla.bar(2)
time.sleep(3)
bla.bar(3)
bla.bar(4)

输出:

foo: 1
foo: 3
foo: 4
bar: 1
bar: 2
bar: 3

编辑:我更改了代码,通过将其存储放入被调用函数的 self 参数中,它可以独立地为多个实例工作。请注意,这完全依赖于名为“self”的第一个参数,但如果您需要更多安全性,您可以搜索一种更可靠的方法来检测修饰的可调用对象是方法还是函数。

EDIT2:如果您执行instance1.foo() 然后尝试执行instance2.foo.setRemaining(0),这可能会出现错误。由于没有切换上下文,这将设置 instance1 的剩余值。可以通过使 setter 和 getter 将方法绑定到上下文来修复,但这会变得混乱。我暂时停在这里

【讨论】:

不幸的是,我认为这种方法也不支持remaining 属性。 现在可以了。我只是在装饰函数中添加了 getter 和 setter。不幸的是,我无法弄清楚如何(或什至)将 getter 和 setter 作为属性附加 查看我对原始问题的编辑,我很粗心,没有考虑清楚,我的原始代码没有按我的预期工作。每个实例都需要有单独的冷却时间。这甚至可能吗? :D 是的,明白了,删除了我的评论 这真的很好,但我想我会采用@interjay 的方法,因为这允许我将remaining 保留为属性。非常感谢您的努力,感谢您,我实际上学到了一些新东西! :) 我已经给了你一个赞成票,但如果可以的话,我会再给你一个。【参考方案3】:

此装饰器可与函数和方法一起使用,支持remaining 属性并作为单个类实现。

import time

class cooldown:
    def __init__(self, timeout):
        self.timeout = timeout
        self.calltime = time.time() - timeout
        self.func = None
        self.obj = None
    def __call__(self, *args, **kwargs):
        if self.func is None:
            self.func = args[0]
            return self
        now = time.time()
        if now - self.calltime >= self.timeout:
            self.calltime = now
            if self.obj is None:
                return self.func.__call__(*args, **kwargs)
            else:
                return self.func.__get__(self.obj, self.objtype)(*args, **kwargs)
    def __get__(self, obj, objtype):
        self.obj = obj
        self.objtype = objtype
        return self
    @property
    def remaining(self):
        now = time.time()
        delta = now - self.calltime
        if delta >= self.timeout:
            return 0
        return self.timeout - delta
    @remaining.setter
    def remaining(self, value):
        self.calltime = time.time() - self.timeout + value
# test with functions
@cooldown(8)
def test(*args):
    print('Function', *args)

>>> test()
Function
>>> test()
>>> test.remaining
4.718205213546753
>>> test.remaining = 0
>>> test()
Function
# test with methods
class A:
    def __init__(self, value):
        self.value = value
    @cooldown(5)
    def a(self, *args):
        print('Method', self.value, *args)

>>> a = A(7)
>>> a.a()
Method 7
>>> a.a()
>>> a.a.remaining
3.589237892348223
>>> a.a.remaining = 10
>>> a.a(32)
>>> a.a.remaining
8.423482288923785
>>> a.a.remaining = 0
>>> a.a(32)
Method 7 32

【讨论】:

以上是关于修改冷却装饰器以适用于方法而不是函数的主要内容,如果未能解决你的问题,请参考以下文章

实现 python 装饰器以将函数传递给另一个函数

装饰器适用于函数但不适用于类

为啥装饰器模式适用于指针而不适用于引用?

python 装饰器

第十篇:装饰器

将装饰器附加到类中的所有函数