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

时间:2015-11-27 00:20:30

标签: python python-3.x python-decorators

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

>>> @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)或其他内容):< / p>

>>> 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(),因为冷却时间是{{1}方法而不是实例。我可能会为此使用某种字典,但在实现之后我更加迷失......

3 个答案:

答案 0 :(得分: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

答案 1 :(得分:1)

解决问题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参数中来独立地处理多个实例。请注意,这纯粹依赖于名为&#34; self&#34;的第一个参数,但如果您需要更高的安全性,可以搜索更健壮的方法来检测装饰的callable是方法还是函数。 / p>

EDIT2:如果您执行instance1.foo()然后尝试执行instance2.foo.setRemaining(0),则可能存在错误。由于上下文没有被切换,这将设置instance1的剩余值。可以通过将setter和getters绑定方法绑定到上下文来修复,但这变得很混乱。我现在就到此为止

答案 2 :(得分:0)

这个装饰器兼容函数和方法,支持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