Search code examples
pythonpython-3.xpython-decorators

Is there a simpler syntax for member decorators for methods?


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

Solution

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