Decorators are a powerful and useful tool in Python that allows you to modify the behavior of a function or class method. They are often used to add functionality to existing code in a clean and readable way. In this section, we will cover the following topics:
- What are Decorators?
 - Creating Simple Decorators
 - Using 
functools.wraps - Decorating Functions with Arguments
 - Class Decorators
 - Practical Examples
 - Exercises
 
- What are Decorators?
 
A decorator is a function that takes another function and extends its behavior without explicitly modifying it. Decorators are commonly used for logging, enforcing access control and permissions, instrumentation, caching, and more.
Basic Syntax
def decorator_function(original_function):
    def wrapper_function():
        # Code to execute before the original function
        print("Wrapper executed this before {}".format(original_function.__name__))
        return original_function()
    return wrapper_function
- Creating Simple Decorators
 
Let's create a simple decorator to understand how it works.
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!")
say_hello()Explanation
my_decoratoris a function that takes another functionfuncas an argument.wrapperis an inner function that adds some behavior before and after callingfunc.- The 
@my_decoratorsyntax is a shorthand forsay_hello = my_decorator(say_hello). 
Output
Something is happening before the function is called. Hello! Something is happening after the function is called.
- Using 
functools.wraps 
functools.wrapsWhen you use decorators, the original function's metadata (like its name, docstring, etc.) is lost. To preserve this metadata, you can use functools.wraps.
Example
from functools import wraps
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper
@my_decorator
def say_hello():
    """A simple function to say hello"""
    print("Hello!")
print(say_hello.__name__)  # Output: say_hello
print(say_hello.__doc__)   # Output: A simple function to say hello
- Decorating Functions with Arguments
 
Decorators can also be used with functions that take arguments.
Example
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper
@my_decorator
def greet(name):
    print(f"Hello, {name}!")
greet("Alice")Output
Something is happening before the function is called. Hello, Alice! Something is happening after the function is called.
- Class Decorators
 
Decorators can also be applied to classes. A class decorator is a function that takes a class as an argument and returns a new class or modifies the existing class.
Example
def class_decorator(cls):
    class WrappedClass(cls):
        def new_method(self):
            return "New method added by decorator"
    return WrappedClass
@class_decorator
class MyClass:
    def original_method(self):
        return "Original method"
obj = MyClass()
print(obj.original_method())  # Output: Original method
print(obj.new_method())       # Output: New method added by decorator
- Practical Examples
 
Logging Decorator
def log_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Function {func.__name__} called with arguments {args} and {kwargs}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper
@log_decorator
def add(a, b):
    return a + b
add(3, 5)Timing Decorator
import time
def timer_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function {func.__name__} took {end_time - start_time} seconds to execute")
        return result
    return wrapper
@timer_decorator
def slow_function():
    time.sleep(2)
    return "Finished"
slow_function()
- Exercises
 
Exercise 1: Create a Simple Decorator
Create a decorator named uppercase_decorator that converts the result of a function to uppercase.
def uppercase_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper
@uppercase_decorator
def greet(name):
    return f"Hello, {name}"
print(greet("Alice"))  # Output: HELLO, ALICEExercise 2: Create a Timing Decorator
Create a decorator named execution_time_decorator that prints the execution time of a function.
import time
def execution_time_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Execution time: {end_time - start_time} seconds")
        return result
    return wrapper
@execution_time_decorator
def slow_function():
    time.sleep(1)
    return "Done"
print(slow_function())  # Output: Execution time: 1.0xxxx secondsExercise 3: Create a Logging Decorator
Create a decorator named logging_decorator that logs the arguments and return value of a function.
def logging_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with arguments {args} and {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper
@logging_decorator
def multiply(a, b):
    return a * b
print(multiply(3, 4))  # Output: Calling multiply with arguments (3, 4) and {}
                       #         multiply returned 12Conclusion
In this section, we have learned about decorators in Python, how to create simple decorators, use functools.wraps to preserve function metadata, decorate functions with arguments, and apply decorators to classes. We also explored practical examples and exercises to reinforce the concepts. Decorators are a powerful tool that can help you write cleaner and more maintainable code. In the next topic, we will delve into generators and their use cases.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
 - Setting Up the Development Environment
 - Python Syntax and Basic Data Types
 - Variables and Constants
 - Basic Input and Output
 
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
 - Function Arguments
 - Lambda Functions
 - Modules and Packages
 - Standard Library Overview
 
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error Handling and Exceptions
Module 8: Advanced Topics
- Decorators
 - Generators
 - Context Managers
 - Concurrency: Threads and Processes
 - Asyncio for Asynchronous Programming
 
Module 9: Testing and Debugging
- Introduction to Testing
 - Unit Testing with unittest
 - Test-Driven Development
 - Debugging Techniques
 - Using pdb for Debugging
 
Module 10: Web Development with Python
- Introduction to Web Development
 - Flask Framework Basics
 - Building REST APIs with Flask
 - Introduction to Django
 - Building Web Applications with Django
 
Module 11: Data Science with Python
- Introduction to Data Science
 - NumPy for Numerical Computing
 - Pandas for Data Manipulation
 - Matplotlib for Data Visualization
 - Introduction to Machine Learning with scikit-learn
 
