I changed a class
which had a function that had to be ran prior to running a number of other functions. The "prior-to-others" function is now a decorator. But the syntax, which I came up with, seems very unintuitive.
It used to be something like this:
class Session:
def __init__(self, ts):
self.tempo_throttlers = [TempoThrottler(t) for t in ts]
...
def _wait_on_throttlers(self):
for th in self.tempo_throttlers:
if not th.isallowed():
time.sleep(th.mustwait())
th.consume()
...
def request1(self):
self._wait_on_throttlers()
...
def request2(self):
self._wait_on_throttlers()
...
And now it's like this:
class Session:
def __init__(self, ts):
self.tempo_throttlers = [TempoThrottler(t) for t in ts]
...
def _wait_on_throttlers(self):
for th in self.tempo_throttlers:
if not th.isallowed():
time.sleep(th.mustwait())
th.consume()
...
def _throttled(f):
def inner(self, *args, **kwargs):
self._wait_on_throttlers()
return f(self, *args, **kwargs)
return inner
@_throttled
def request1(self):
...
@_throttled
def request2(self):
...
And, while I think the use of this decorator made the code more clear, the implementation of this decorator took some doing. It's also very fragile and hard to read. For example, if the inner return line return f(self, *args, **kwargs)
is changed to return self.f(*args, **kwargs)
, then it won't work anymore.
This seems to do with the order in which the elements of the class are compiled. I am also afraid that this would break in future versions of Python. I am using Python 3.6.8.
Is there an accepted and/or recommended way to make such class-member decorators of class methods which would be less counter-intuitive and less fragile?
For the sake of a minimal reproducible example, the ...
can be considered to be a pass
statement and the class TempThrottler
can be defined as below (this isn't the actual implementation, but it's enough to satisfy the example above):
class TempoThrottler:
def __init__(self, t):
pass
def isallowed(self):
from random import randint
return (True, False)[randint(0,1)]
def mustwait(self):
return 1
def consume(self):
pass
Below is a runnable example that illustrates my suggestion of how it would be possible to move the decorator function completely out of the class:
from random import randint
import time
class TempoThrottler:
def __init__(self, t):
pass
def isallowed(self):
# return [True, False](randint(0,1))
return [True, False][randint(0,1)]
def mustwait(self):
return 1
def consume(self):
pass
# Decorator not in class.
def _throttled(f):
def inner(self, *args, **kwargs):
self._wait_on_throttlers()
return f(self, *args, **kwargs)
return inner
class Session:
def __init__(self, ts):
self.tempo_throttlers = [TempoThrottler(t) for t in ts]
...
def _wait_on_throttlers(self):
for th in self.tempo_throttlers:
if not th.isallowed():
time.sleep(th.mustwait())
th.consume()
...
@_throttled
def request1(self):
print('in request1()')
...
@_throttled
def request2(self):
print('in request2()')
...
s = Session(range(3))
s.request1() # -> in request1()
s.request2() # -> in request2()