Application of Decorators (1): Control How Often a Function Can be Called in Python

In some practical cases, how often a specific function can be invoked may need to be controlled. This post demonstrates how this can be done in python by using Decorator.

Assume we don’t want the function to be called more than some certain number of times within a given duration. In the closure the decorator returns, we define a list for storing the time at which the function is called. For the convenience of description, we call the time at which the function is called the invocation time. When the number of invocation times exceeds the maximum we wanted to control within the given duration, the time difference between the current invocation time and the first one in the list is checked. If it is less than the given duration, the function will not be invoked. Otherwise, the current invocation time is appended to the list, and the first item in the list is removed. Below is an example implementation of the decorator.

def control_freq(duration: float=0.0, max_count: int=0):
def outer(fnc):
from functools import wraps
import time
ts_queue = []
@wraps(fnc)
def inner(*args, **kwargs):
nonlocal ts_queue
if duration <= 0 or max_count <= 0:
return fnc(*args, **kwargs)
current_time = time.time()
if len(ts_queue) == max_count:
tdiff = current_time - ts_queue[0]
if tdiff < duration:
fnc_str = [str(arg) for arg in args] + \
[f'{k}={v}' for (k, v) in kwargs.items()]
fnc_str = ','.join(fnc_str)
fnc_str = fnc.__name__ + '(' + fnc_str + ')'
print(f'{fnc_str} is invoked too soon!')
return None
else:
del ts_queue[0]
ts_queue.append(current_time)
return fnc(*args, **kwargs)
return inner
return outer

With the above decorator, we can control during a given duration, how many times at maximum a function can be called. Below are two examples.

Define a function add(), and set that within once second, it can be called up to twice.

@control_freq(1, 2)
def add(a: int, b: int) -> int:
return a + b

Now call the function add() for three times within one second. As we can see below, the third call gets skipped.

>>> def test():
... start = time.perf_counter()
... for i in range(3):
... print(f'{i} + {i+1} = {add(i, i+1)}')
... elapsed = time.perf_counter() - start
... print('Elapsed time: {0:.2f}s'.format(elapsed))
...
>>> test()
0 + 1 = 1
1 + 2 = 3
add(2,3) is invoked too soon!
2 + 3 = None
Elapsed time: 0.01s
>>>

Define a function subtract(), and set that within two seconds, it can be called only once.

@control_freq(2, 1)
def subtract(a: int, b: int) -> int:
return a - b

Now call the function subtract() for three times within one second. Both the second and third calls are all skipped.

>>> def test():
... start = time.perf_counter()
... for i in range(3):
... print(f'{i+1} - {i} = {subtract(i+1, i)}')
... elapsed = time.perf_counter() - start
... print('Elapsed time: {0:.2f}s'.format(elapsed))
...
>>> test()
1 - 0 = 1
subtract(2,1) is invoked too soon!
2 - 1 = None
subtract(3,2) is invoked too soon!
3 - 2 = None
Elapsed time: 0.01s
>>>

Written by

Senior Software Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store