In Python, decorators are a powerful tool that allows you to modify or extend the behavior of functions or classes without changing their actual code. Decorators are widely used in Python frameworks for tasks such as logging, access control, and caching.
1. What is a Python Decorator?
A decorator is a function that takes another function as an argument, adds some functionality to it, and returns the modified function. Decorators are often used to wrap another function in order to modify its behavior.
Basic Syntax of a Decorator:
def decorator_function(original_function):
    def wrapper_function(*args, **kwargs):
        # Add functionality before the original function
        print(f"Wrapper executed before {original_function.__name__}")
        result = original_function(*args, **kwargs)
        # Add functionality after the original function
        print(f"Wrapper executed after {original_function.__name__}")
        return result
    return wrapper_function
2. Creating and Using a Decorator
Example:
def my_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
@my_decorator
def say_hello():
    print("Hello!")
# Call the decorated function
say_hello()
In this example, my_decorator adds extra functionality before and after the say_hello function.
3. Using Decorators with Arguments
You can also create decorators that accept arguments.
Example:
def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator
@repeat(3)
def greet():
    print("Hello!")
# This will print "Hello!" three times
greet()
4. Practical Use Cases for Decorators
- Logging: Automatically log function calls.
- Authorization: Restrict access to certain parts of the code.
- Caching: Store function results for faster access.
- Timing: Measure the execution time of functions.
Example: Logging Decorator
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Function '{func.__name__}' is called with arguments {args} and {kwargs}")
        return func(*args, **kwargs)
    return wrapper
@log_decorator
def add(a, b):
    return a + b
# Call the decorated function
result = add(3, 5)
print(f"Result: {result}")
5. Class-Based Decorators
Decorators can also be implemented using classes by defining the __call__ method.
Example:
class MyDecorator:
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        print("Class-based decorator: Before the function call.")
        result = self.func(*args, **kwargs)
        print("Class-based decorator: After the function call.")
        return result
@MyDecorator
def greet(name):
    print(f"Hello, {name}!")
greet("Alice")
6. Chaining Multiple Decorators
You can apply multiple decorators to a single function. They are applied from bottom to top.
Example:
def decorator_one(func):
    def wrapper(*args, **kwargs):
        print("Decorator One")
        return func(*args, **kwargs)
    return wrapper
def decorator_two(func):
    def wrapper(*args, **kwargs):
        print("Decorator Two")
        return func(*args, **kwargs)
    return wrapper
@decorator_one
@decorator_two
def say_hello():
    print("Hello!")
say_hello()
In this example, decorator_two is applied first, followed by decorator_one.
Conclusion
Python decorators are a powerful tool for modifying and extending the behavior of functions. They help keep your code clean and reusable. Whether you’re adding logging, handling access control, or measuring performance, decorators provide an elegant solution.
