w3resource

Understanding and creating Python Decorators

Introduction to Python Decorators

Decorators in Python are a powerful and expressive way to modify or enhance functions and methods without changing their code. They allow us to wrap another function in order to extend the behavior of the wrapped function, often used for logging, access control, memoization, and more.

This tutorial will introduce the concept of decorators through practical examples. These examples provide a foundational understanding of Python decorators and their various uses. By mastering decorators, you can write cleaner, more maintainable, and expressive code, enhancing the functionality of your functions in a modular and reusable way.

Example 1: Basic Decorator

This example demonstrates the simplest form of a decorator that wraps a function and adds functionality before and after the original function is called.

Code:

# Define a basic decorator
def simple_decorator(func):
    def wrapper():
        print("Before calling the function.")
        func()  # Call the original function
        print("After calling the function.")
    return wrapper

# Apply the decorator to a function
@simple_decorator
def say_hello():
    print("Hello, world!")

# Call the decorated function
say_hello()

Output:

Before calling the function.
Hello, world!
After calling the function.

Explanation:

  • simple_decorator(func): This is a basic decorator that takes a function as an argument.
  • wrapper(): Inner function that wraps the original function, adding code before and after the function call.
  • @simple_decorator: This syntax applies simple_decorator to say_hello.
  • Result: When 'say_hello()' is called, it prints additional messages before and after the original function's message.

Example 2: Decorator with Arguments

This example shows how to create a decorator that can handle functions with arguments.

Code:

# Define a decorator that can handle arguments
def decorator_with_args(func):
    def wrapper(*args, **kwargs):
        print(f"Arguments passed to function: {args}, {kwargs}")
        return func(*args, **kwargs)  # Call the original function with its arguments
    return wrapper

# Apply the decorator to a function with arguments
@decorator_with_args
def add(x, y):
    return x + y

# Call the decorated function
result = add(30, 82)
print(f"Result of add function: {result}")

Output:

Arguments passed to function: (30, 82), {}
Result of add function: 112

Explanation:

  • wrapper(*args, **kwargs): The wrapper function uses *args and **kwargs to pass any number of positional and keyword arguments to the decorated function.
  • Result: When calling add(30, 82), the decorator prints the arguments passed to the function and then the result.

Example 3: Using functools.wraps to Preserve Metadata

This example demonstrates how to use functools.wraps to preserve the original function's metadata when applying a decorator.

Code:

import functools

# Define a decorator that uses functools.wraps
def preserve_metadata(func):
    @functools.wraps(func)  # Preserves metadata of the original function
    def wrapper(*args, **kwargs):
        print("Calling the wrapped function...")
        return func(*args, **kwargs)
    return wrapper

# Apply the decorator
@preserve_metadata
def multiply(x, y):
    """Multiplies two numbers."""
    return x * y

# Call the decorated function and check metadata
result = multiply(12, 8)
print(f"Result of multiply function: {result}")
print(f"Function name: {multiply.__name__}")
print(f"Function docstring: {multiply.__doc__}")

Output:

Calling the wrapped function...
Result of multiply function: 96
Function name: multiply
Function docstring: Multiplies two numbers.

Explanation:

  • @functools.wraps(func): Decorator that updates the wrapper function to look like func by copying over the function name, docstring, and other metadata.
  • Result: The decorated function retains the original name and docstring, which is useful for documentation and debugging.

Example 4: Basic Class Decorator

This example explains how to create decorators that accept their own parameters, providing more control over their behavior.

Code:

import functools
# Define a decorator that accepts parameters
def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)  # Call the function multiple times
        return wrapper
    return decorator_repeat

# Apply the decorator with a parameter
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

# Call the decorated function
greet("Misao")

Output:

Hello, Misao!
Hello, Misao!
Hello, Misao!

Explanation:

  • repeat(num_times): Outer function that takes a parameter num_times.
  • decorator_repeat(func): Inner decorator function that wraps the original function.
  • Result: When greet("Alice") is called, it repeats the greeting three times due to the parameter passed to the decorator.

Example 5: Applying Multiple Decorators

This example shows how to apply multiple decorators to a single function.

Code:

# Define two simple decorators
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

# Apply multiple decorators
@decorator_one
@decorator_two
def display_message():
    print("This is the main function.")

# Call the decorated function
display_message()

Output:

Decorator One
Decorator Two
This is the main function.

Explanation:

Order of Application: '@decorator_two' is applied first, then '@decorator_one'.

Result: When calling 'display_message()', the output shows the order of decorators as applied, followed by the function's message.

Example 6: Using Decorators for Access Control

This example demonstrates using a decorator for access control, such as restricting access based on user roles.

Code:

# Define a decorator for access control
def require_admin(func):
    def wrapper(user):
        if user == "admin":
            return func(user)
        else:
            print("Access denied.")
    return wrapper

# Apply the decorator to a function
@require_admin
def view_dashboard(user):
    print(f"Welcome, {user}! You have access to the dashboard.")
# Call the decorated function
view_dashboard("admin")    # Admin access
view_dashboard("guest")    # Access denied

Output:

Welcome, admin! You have access to the dashboard.
Access denied.

Explanation:

  • require_admin(func): Decorator that checks if the user has admin rights before allowing access to the function.
  • Access Control Logic: Only allows the function to run if the user is "admin".
  • Result: Demonstrates how decorators can add simple access control mechanisms to functions.


Become a Patron!

Follow us on Facebook and Twitter for latest update.