In the Python programming landscape, decorators emerge as a formidable and adaptable mechanism for modifying function or method behaviors. At their core, decorators are functions designed to take another function, extending its capabilities without directly altering its original implementation. This might initially seem complex, so let’s demystify this concept through practical demonstrations.
A decorator in Python is a function that wraps another function or method. The primary purpose is to transparently modify or enhance the behavior of the function being wrapped. Think of it as packaging that adds some goodies to an existing present without changing the present itself.
To grasp the fundamentals, consider a straightforward decorator that announces when a function is invoked.
def simple_decorator(func):
def wrapper():
print("Function is being called")
func()
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
>>> Function is being called
>>> Hello!
Here, simple_decorator acts as a precursor to the actual say_hello function, signalling its execution.
Next, let’s explore a practical decorator that calculates a function’s execution duration, an invaluable tool for performance analysis.
import time
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Executed '{func.__name__}' in {end_time - start_time:.6f}s")
return result
return wrapper
@timer
def sleep_and_print(seconds):
time.sleep(seconds)
print(f"Slept {seconds}s")
return seconds
sleep_and_print(1)
>>> Slept 1s
>>> Executed 'sleep_and_print' in 1.005299s
This timer decorator reports the duration of the sleep_and_print function's execution, showcasing its versatility by accepting any number of arguments.
Enhancing our timer decorator further, we introduce logic to differentiate between standard functions and class methods through introspection with hasattr.
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
exec_time = end_time - start_time
if args and hasattr(args[0], func.__name__):
class_name = args[0].__class__.__name__
print(f"'{class_name}.{func.__name__}' executed in {exec_time:.6f}s")
return result
print(f"'{func.__name__}' executed in {exec_time:.6f}s")
return result
return wrapper
Thanks to the if statement, we can apply the decorator to both, class methods:
class Calculator:
@timer
def add(self, a, b):
return a + b
calc = Calculator()
calc.add(1, 2)
>>> 'Calculator.add' executed in 0.000001s
And standard functions:
@timer
def add(a, b):
return a + b
add(a=1, b=2)
>>> 'add' executed in 0.000002s
Decorators stand out as an elegant feature in Python, providing a streamlined and intuitive approach to enhancing function functionalities. From performance timing to logging and access control, decorators enable developers to enrich their code’s behavior in a Pythonic manner, without compromising the integrity of the original logic. While we’ve only scratched the surface with these examples, the potential applications for decorators are vast and varied, opening up a world of possibilities for creative and efficient programming. Embrace the power of decorating and discover the myriad ways it can transform your code.