Python Decorators
Introduction
Decorator is a nested function that receives another function as an argument by using the @ symbol. Decorators can be utilized to print, debug, time and run checks on values as shown in this page.
Background
In Python, functions can be nested:
def welcome_msg(): def inner(): return "Welcome" return inner
It could also get a function (and its parameters) as arguments:
def welcome_msg(func): def inner(*args, **kwargs): # Print function name print(f"Welcome from {func.__name__}") # Execute function return func(*args, **kwargs) return inner
It can be used as follows:
@welcome_msg def hello_world(): print("Hello World") hello_world()
In this case the output is:
Welcome from hello_world Hello World
The decorator (@welcome_msg) was executed first receiving hello_world
as an argument. This decorator only prints a welcome message: Welcome from {func.__name__}
where func.__name__
is the name of the decorated function, so it is hello_world
. Then, the function is executed and one can see that Hello World
was printed. This functionality will be used in the next examples to show debugging, timing and running validation checks.
Timing
One can utilize decorators to print the time before and after function execution as can be seen below:
from datetime import datetime DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' def timeit(func): def inner(*args, **kwargs): # Print 'DATE HOUR:MINUTE:SECOND' before execution print(f"Started Execution: {datetime.now().strftime(DATETIME_FORMAT)}") # Execute function var = func(*args, **kwargs) # Print `DATE HOUR:MINUTE:SECOND' after execution print(f"Finished Execution: {datetime.now().strftime(DATETIME_FORMAT)}") return var return inner
For example, a program that count to num
(with 1 second delay between each count).
import time @timeit def counter(num, delay = 1): for i in range(num): print(i+1) time.sleep(delay) counter(5)
Output:
Started Execution: 2024-02-04 11:12:17 1 2 3 4 5 Finished Execution: 2024-02-04 11:12:22
One can calculate execution time instead of printing time before and after execution. Consider the following timeit2
decorator:
def timeit2(func): def inner(*args, **kwargs): start = datetime.now() # Execute function var = func(*args, **kwargs) end = datetime.now() # Calculate execution time duration = end-start args_str = ','.join([str(i) for i in args]) print(f"{func.__name__}({args_str}) Execution Time: {str(duration).split('.')[0]}") return var return inner @timeit2 def counter(num, delay = 1): for i in range(num): print(i+1) time.sleep(delay) counter(10)
Output:
1 2 3 4 5 6 7 8 9 10 counter(10) Execution Time: 0:00:10
Debugging
Decorators can be used to print the values of arguments passed to a function and the returned value:
import inspect def debug(func): def inner(*args, **kwargs): # Print function's signature for reference. # Parameters with default values are not accessible # via either *args or kwargs, so this line is meant for those # parameters to ensure they are taken into account in debugging. print(f'Signature: {func.__name__}{inspect.signature(func)}') # Convert *args and **kwargs into strings arguments = ",".join([str(i) for i in args]) keyworded_args = ",".join([key + "=" + str(value) for key,value in kwargs.items()]) # Print Execution signature. print(f'Execution: {func.__name__}({arguments}{"," + keyworded_args if keyworded_args else ""})') # Execute function var = func(*args, **kwargs) # Print results print(f'Returned Value: {str(var)}') return var return inner @debug def summation(upper, lower = 1): return sum([i for i in range(lower, upper + 1)]) summation(3)
Summation was implemented and was debugged using the @debug
decorator which prints the function's signature, values of arguments at the time of execution, and the returned value:
Signature: summation(upper, lower=1) Execution: summation(3) Returned Value: 6
Validation Checks
Ensures that all arguments are valid (type, range, etc.) and prevents execution of the decorated function by raising an error. It can enhance debugging since the source of the error is known given error messages are clear. I have been using this technique for database client to ensure that the program is connected to database.
def validate(func): def inner(*args): invalid_arg = None for arg in args: if not isinstance(arg, int): raise TypeError("Both Upper and Lower bounds of summation must be integers!") return inner @validate def summation(upper, lower = 1): return sum([i for i in range(lower, upper + 1)]) summation(3.5)
output:
TypeError: Both Upper and Lower bounds of summation must be integers!
Example - SQLiteClient
Validation checks were used in Context Manager page where I created two decorators: is_conn
(line 141 in Final Code section) to check connection, and check_args
(line 152 in Final Code section) to validate arguments.
They can be merged into one decorator, but I wanted to keep the separate for convenience.