Changing the cooldown decorator to work instead of methods instead

I am trying to create a decorator that will work for methods to apply a “cooldown” to them, that is, they cannot be called several times for a certain duration. I already created one for the functions:

>>> @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 

but I need this to support class methods instead of regular functions:

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

Here is what I created to make it work for normal functions:

 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 

But this does not work with methods, because it passes the _CooldownFunction object as self and completely ignores the original self . How do I get it to work with methods by correctly passing the original self instead of the _CooldownFunction object?

In addition, users should be able to change the remaining time on the fly, which makes it even more difficult (cannot just use __get__ to return functools.partial(self.__call__, obj) or something else):

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

Edit: It should work only for methods, not for both methods and functions.

Edit 2: There is a huge flaw in this design. Although it works fine for normal functions, I want it to decorate each instance separately. Currently, if I needed to have two instances of t1 and t2 and had to call t1.f() , I could no longer call t2.f() because the recovery time was teid for the f() method instead of instances. I could probably use some kind of vocabulary for this, but after this realization I lost even more ...

+6
source share
3 answers

You can override the __get__ class method to make it a descriptor. The __get__ method will be called when someone receives the decorated method from its containing object and the containing object is passed, which you can then go to the original method. It returns an object that implements the necessary functions.

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

The Wrapper object implements __call__ and any properties you want, so move these implementations to this object. It will look like this:

 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
source

Fixing the interjay problem, I quickly rewrote your cooldown decorator, which now works for all kinds of functions / methods:

 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 

Tests:

 @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) 

outputs:

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

EDIT: I changed the code, so it works independently for multiple instances, putting it in the argument of the called function self . Note that this purely relies on the first argument called “I,” but you can look for a more reliable way to detect if the decorated method or function is decorated, if you need extra security here.

EDIT2: Perhaps you have an error if you execute instance1.foo() and then try to do instance2.foo.setRemaining(0) . Since the context did not switch, this would set the remaining value for instance1. It can be fixed if setters and getters bound the methods to the context, but this becomes messy. I will stay here for now

+1
source

This decorator works with both functions and methods, supports the remaining property remaining and is implemented as a single class.

 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) >>> aa() Method 7 >>> aa() >>> aaremaining 3.589237892348223 >>> aaremaining = 10 >>> aa(32) >>> aaremaining 8.423482288923785 >>> aaremaining = 0 >>> aa(32) Method 7 32 
0
source

All Articles