Python Decorators

Decorators in Python are a powerful and versatile feature that allows you to modify the behavior of a function or class method. They are commonly used for logging, enforcing access control, instrumentation, caching, and more.

1. What is a Decorator?

A decorator is a function that takes another function (or method) as an argument and extends its behavior without explicitly modifying it. Decorators are often used in a syntactic sugar format using the `@decorator_name` syntax, making it easier to apply them to functions.

2. Creating a Simple Decorator

Example: A simple decorator that logs the execution of a function.
def simple_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@simple_decorator
def say_hello():
    print("Hello!")

say_hello()

Output:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.

Explanation: Here, `simple_decorator` takes `say_hello` as an argument, wrapping it in the `wrapper` function. The `wrapper` executes code before and after calling `say_hello`, demonstrating how decorators can extend functionality.

3. Decorators with Arguments

Sometimes, you may want to create decorators that accept arguments. To do this, you need an additional outer function.

Example: A decorator that accepts a message.
def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator_repeat

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Output:
Hello, Alice!
Hello, Alice!
Hello, Alice!

Explanation: The `repeat` function takes an argument (`num_times`) and returns a `decorator_repeat` function, which defines the actual decorator. The `wrapper` function calls the decorated function the specified number of times.

4. Decorators for Class Methods

Decorators can also be used to modify methods in classes.

Example: A decorator that logs method calls in a class.
def log_method_call(func):
    def wrapper(self, *args, **kwargs):
        print(f"Calling method: {func.__name__}")
        return func(self, *args, **kwargs)
    return wrapper

class MyClass:
    @log_method_call
    def say_hello(self):
        print("Hello from MyClass!")

obj = MyClass()
obj.say_hello()

Output:
Calling method: say_hello
Hello from MyClass!

Explanation: The `log_method_call` decorator wraps the `say_hello` method, logging a message whenever it is called.

5. Using Built-in Decorators

Python comes with several built-in decorators, such as `@staticmethod`, `@classmethod`, and `@property`.

a. @staticmethod
A static method does not receive an implicit first argument (like `self` or `cls`).
class Math:
    @staticmethod
    def add(x, y):
        return x + y

print(Math.add(5, 3))

Output:
8


b. @classmethod
A class method receives the class as its implicit first argument.
class Math:
    @classmethod
    def multiply(cls, x, y):
        return x * y

print(Math.multiply(4, 5))

Output:
20


c. @property
The property decorator allows you to define a method that can be accessed like an attribute.
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14 * (self.radius ** 2)

circle = Circle(5)
print(circle.area)  # No parentheses needed

Output:
78.5


6. Chaining Decorators

You can apply multiple decorators to a single function by stacking them.

Example: Using both logging and repetition decorators.
def log(func):
    def wrapper(*args, **kwargs):
        print(f"Function {func.__name__} is called")
        return func(*args, **kwargs)
    return wrapper

@repeat(2)
@log
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Bob")

Output:
Function say_hello is called
Hello, Bob!
Function say_hello is called
Hello, Bob!

Explanation: The order of decorators matters. Here, the `log` decorator is applied first, followed by the `repeat` decorator.

7. The Functools Module

Python’s `functools` module provides utilities to work with decorators. The `@functools.wraps` decorator is commonly used to preserve the metadata of the original function.
from functools import wraps

def log_with_wraps(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_with_wraps
def add(x, y):
    return x + y

print(add(3, 4))
print(add.__name__)  # Check the name of the function

Output:
Calling add
7
add

Explanation: By using `@wraps`, the `wrapper` function retains the name and docstring of the original `add` function.

8. Real-World Use Cases of Decorators

1. Access Control: Restrict access to certain functions based on user permissions.

2. Logging: Automatically log function calls and their arguments for debugging.

3. Caching: Cache results of expensive function calls to improve performance.

4. Timing Functions: Measure the time a function takes to execute.

Example: A simple timer decorator.
import time

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timer
def compute_square_sum(n):
    return sum(i * i for i in range(n))

compute_square_sum(100000)

Output:
compute_square_sum took 0.0020 seconds


9. Conclusion

Decorators are a powerful feature in Python that enhance code reusability and readability. They allow you to modify the behavior of functions and methods dynamically. With the ability to create simple decorators, use built-in decorators, chain decorators, and apply advanced techniques with `functools`, decorators can greatly simplify complex tasks and provide clean solutions to various programming challenges.

Previous: Python Operator Overloading | Next: Python Context Manager

<
>