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:

  1. What are Decorators?
  2. Creating Simple Decorators
  3. Using functools.wraps
  4. Decorating Functions with Arguments
  5. Class Decorators
  6. Practical Examples
  7. Exercises

  1. 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

  1. 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_decorator is a function that takes another function func as an argument.
  • wrapper is an inner function that adds some behavior before and after calling func.
  • The @my_decorator syntax is a shorthand for say_hello = my_decorator(say_hello).

Output

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

  1. Using functools.wraps

When 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

  1. 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.

  1. 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

  1. 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()

  1. 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, ALICE

Exercise 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 seconds

Exercise 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 12

Conclusion

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

Module 2: Control Structures

Module 3: Functions and Modules

Module 4: Data Structures

Module 5: Object-Oriented Programming

Module 6: File Handling

Module 7: Error Handling and Exceptions

Module 8: Advanced Topics

Module 9: Testing and Debugging

Module 10: Web Development with Python

Module 11: Data Science with Python

Module 12: Final Project

© Copyright 2024. All rights reserved