Home » Python Decorators Tutorial with Examples

Python Decorators Tutorial with Examples

Decorators are a powerful and flexible feature in Python that allows you to modify the behavior of a function or a class.

In simple terms, a decorator is a function that takes another function and extends or alters its behavior without modifying it directly.

Decorators are often used to add functionality like logging, access control, performance measurement, or memoization to existing functions.

In this tutorial, you’ll learn:

What are decorators?
How to create and use function decorators
Using multiple decorators
Decorating functions with arguments
Class-based decorators
Practical examples of decorators

1. What Are Decorators?

A decorator is a function that takes another function as input and returns a new function that extends the behavior of the original one. This is done without modifying the original function’s code.

Here is a simple template for a decorator:

def my_decorator(func):
    def wrapper():
        # Code to run before the function call
        print("Before the function call")

        # Call the original function
        func()

        # Code to run after the function call
        print("After the function call")
    
    return wrapper

When you apply the decorator to a function, it wraps that function with additional functionality.

2. How to Create and Use Function Decorators

Let’s start by creating a simple decorator and applying it to a function.

Example 1: A Simple Decorator

def my_decorator(func):
    def wrapper():
        print("Something before the function")
        func()
        print("Something after the function")
    return wrapper

# Applying the decorator to a function
@my_decorator
def say_hello():
    print("Hello!")

# Call the decorated function
say_hello()

Output:

Something before the function
Hello!
Something after the function

In this example, say_hello() is decorated with my_decorator, which means that the original function is wrapped with extra functionality: printing something before and after the function call.

The @my_decorator syntax is a shortcut for:

say_hello = my_decorator(say_hello)

3. Using Multiple Decorators

You can apply more than one decorator to a function by stacking them. Python applies decorators in a bottom-up order (i.e., the one closest to the function runs first).

Example 2: Stacking Multiple Decorators

def decorator_one(func):
    def wrapper():
        print("Decorator One: Before function")
        func()
        print("Decorator One: After function")
    return wrapper

def decorator_two(func):
    def wrapper():
        print("Decorator Two: Before function")
        func()
        print("Decorator Two: After function")
    return wrapper

@decorator_one
@decorator_two
def greet():
    print("Hello!")

greet()

Output:

Decorator One: Before function
Decorator Two: Before function
Hello!
Decorator Two: After function
Decorator One: After function

In this example, decorator_two runs first because it’s closest to the function, and decorator_one runs around it.

4. Decorating Functions with Arguments

When you need to decorate functions that accept arguments, you must modify the decorator to accept *args and **kwargs. This ensures that your decorator can handle any number and type of arguments.

Example 3: A Decorator for Functions with Arguments

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function")
        result = func(*args, **kwargs)
        print("After the function")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

# Call the decorated function
result = add(5, 3)
print(f"Result: {result}")

Output:

Before the function
After the function
Result: 8

The decorator works with any number of positional (*args) or keyword arguments (**kwargs) that the original function accepts.

5. Class-Based Decorators

While most decorators are functions, you can also create decorators using classes. Class-based decorators are useful when you want more control or need to maintain state.

Example 4: A Class-Based Decorator

class MyDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("Class-based: Before the function")
        result = self.func(*args, **kwargs)
        print("Class-based: After the function")
        return result

@MyDecorator
def multiply(a, b):
    return a * b

result = multiply(5, 4)
print(f"Result: {result}")

Output:

Class-based: Before the function
Class-based: After the function
Result: 20

In this example, the class MyDecorator acts as a decorator by implementing the __call__() method. This allows the class to behave like a function and be used as a decorator.

6. Practical Examples of Decorators

Example 1: Logging Decorator

A logging decorator can be used to log when a function is called and its return value.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned: {result}")
        return result
    return wrapper

@logger
def subtract(a, b):
    return a - b

result = subtract(10, 3)

Output:

Calling function: subtract
Function subtract returned: 7

Example 2: Timing Function Execution

You can use a decorator to measure how long a function takes to execute, which is useful for performance monitoring.

import time

def timer(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:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(2)
    print("Function finished")

slow_function()

Output:

Function finished
Function slow_function took 2.0001 seconds

Example 3: Access Control Decorator

An access control decorator can be used to restrict access to a function based on a condition (e.g., user authentication).

def requires_authentication(func):
    def wrapper(user_authenticated, *args, **kwargs):
        if user_authenticated:
            return func(*args, **kwargs)
        else:
            print("Access Denied: User not authenticated")
    return wrapper

@requires_authentication
def view_dashboard():
    print("Welcome to the dashboard!")

# Test the decorator
view_dashboard(True)   # User is authenticated
view_dashboard(False)  # User is not authenticated

Output:

Welcome to the dashboard!
Access Denied: User not authenticated

Example 4: Caching Results (Memoization)

A decorator can cache the result of expensive function calls and return the cached result when the same inputs are provided again.

def cache(func):
    cached_results = {}
    def wrapper(*args):
        if args in cached_results:
            print("Returning cached result")
            return cached_results[args]
        result = func(*args)
        cached_results[args] = result
        return result
    return wrapper

@cache
def fibonacci(n):
    if n in [0, 1]:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))
print(fibonacci(10))  # Cached result

Output:

55
Returning cached result
55

Conclusion

Decorators are a versatile and powerful feature in Python that allows you to modify the behavior of functions and classes without changing their actual implementation. Here’s a summary of what you’ve learned:

How to create and apply function decorators.
How to apply multiple decorators to a single function.
How to decorate functions that accept arguments using *args and **kwargs.
How to create class-based decorators.
Practical use cases like logging, timing, access control, and caching.

With these concepts, you’re ready to start using decorators to cleanly and efficiently extend your Python functions!

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More