Python Decorators Tutorial

Anastasios Antoniadis

Python decorators are a powerful and flexible feature that allows developers to modify or extend the behavior of functions and methods without altering their code. They are widely used in Python frameworks and libraries, including Flask, Django, and logging modules. This article thoroughly explains Python decorators, their use cases, and practical examples to illustrate their functionality.

What is a Decorator?

A decorator in Python is essentially a function that takes another function as an argument, extends or modifies its behavior, and returns the modified function. Decorators make it easy to add reusable functionality to functions or methods in a clean and readable way.

Basic Syntax of a Decorator

A simple decorator follows this pattern:

def decorator_function(original_function):
    def wrapper_function(*args, **kwargs):
        print(f"Wrapper executed before {original_function.__name__}.")
        return original_function(*args, **kwargs)
    return wrapper_function

You can apply this decorator using the @ symbol:

@decorator_function
def display():
    print("Display function executed.")

# Calling the function
display()

When display() is called, the wrapper_function inside decorator_function executes first, adding additional behavior before calling the original display() function.

Why Use Decorators?

Decorators are useful for:

Code Reusability: They allow behavior to be added to multiple functions without repeating code.

Separation of Concerns: They help in keeping concerns separated by managing cross-cutting concerns like logging, authentication, and caching.

Enhanced Readability: Using decorators makes the code cleaner and more readable.

Understanding Function Wrapping with functools.wraps

One common issue with decorators is that they replace the original function’s metadata (like its name and docstring). To retain these properties, use functools.wraps:

from functools import wraps

def decorator_function(original_function):
    @wraps(original_function)
    def wrapper_function(*args, **kwargs):
        print(f"Wrapper executed before {original_function.__name__}.")
        return original_function(*args, **kwargs)
    return wrapper_function

Using @wraps(original_function) ensures that original_function retains its original name and docstring.

Applying Multiple Decorators

You can stack multiple decorators on a function, where the decorators execute from the innermost to the outermost.

def decorator1(func):
    def wrapper():
        print("Decorator 1")
        func()
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2")
        func()
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Original function execution")

my_function()

This results in the following output:

Decorator 1
Decorator 2
Original function execution

Practical Use Cases of Decorators

1. Logging

Decorators are commonly used for logging function calls:

from functools import wraps

def log_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} executed")
        return result
    return wrapper

@log_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Output:

Calling function greet
Hello, Alice!
Function greet executed

2. Timing Function Execution

Measuring execution time is another common use case:

import time
from functools import wraps

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

@timing_decorator
def compute():
    time.sleep(2)
    print("Computation finished")

compute()

Output:

Computation finished
Execution time: 2.0008 seconds

3. Authentication

Used in web frameworks to restrict access:

from functools import wraps

def authenticate(user_role):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if user_role != "admin":
                print("Access denied")
                return
            return func(*args, **kwargs)
        return wrapper
    return decorator

@authenticate("admin")
def secure_function():
    print("Admin access granted")

secure_function()

Output:

Admin access granted

Class-Based Decorators

A class-based decorator is a class that implements the __call__ method, making the instance of the class callable like a function. This allows the class to modify or enhance the behavior of functions or methods it decorates.

class MyDecorator:
    def __init__(self, func):
        """Initialize with the function to be decorated"""
        self.func = func

    def __call__(self, *args, **kwargs):
        """Modify function behavior before and after calling"""
        print("Before function execution")
        result = self.func(*args, **kwargs)
        print("After function execution")
        return result

@MyDecorator
def say_hello():
    print("Hello, World!")

say_hello()

Explanation:

__init__(self, func): Stores the function to be decorated.

__call__(self, *args, **kwargs): This method allows the instance of the class to be invoked as a function.

When say_hello() is called, the decorator prints a message before and after executing say_hello().

Output:

Before function execution
Hello, World!
After function execution

Using a Class-Based Decorator with Arguments

Sometimes, decorators need parameters. This can be achieved by modifying the class to accept additional arguments.

class RepeatDecorator:
    def __init__(self, num_repeats):
        """Initialize with a parameter for the number of repeats"""
        self.num_repeats = num_repeats

    def __call__(self, func):
        """Wrap the function and repeat it multiple times"""
        def wrapper(*args, **kwargs):
            for _ in range(self.num_repeats):
                func(*args, **kwargs)
        return wrapper

@RepeatDecorator(3)
def greet():
    print("Hello!")

greet()

Explanation:

The decorator accepts num_repeats as an argument.

The __call__ method returns a new wrapper function that repeats the execution num_repeats times.

Output:

Hello!
Hello!
Hello!

Class-Based Decorators with Instance State

Class-based decorators can maintain state using instance attributes. This is useful for tracking function calls.

class CountCalls:
    def __init__(self, func):
        """Initialize with the function and a counter"""
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        """Increment counter and execute function"""
        self.count += 1
        print(f"Call {self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hi():
    print("Hi!")

say_hi()
say_hi()
say_hi()

Explanation:

The count attribute tracks how many times the function has been called.

Each time say_hi() is executed, count is incremented and displayed.

Output:

Call 1 to say_hi
Hi!
Call 2 to say_hi
Hi!
Call 3 to say_hi
Hi!

Using Class-Based Decorators with Methods

Class-based decorators can also be applied to class methods.

class MethodLogger:
    def __call__(self, func):
        """Wraps method execution with logging"""
        def wrapper(*args, **kwargs):
            print(f"Calling method {func.__name__}")
            return func(*args, **kwargs)
        return wrapper

class MyClass:
    @MethodLogger()
    def hello(self):
        print("Hello from MyClass")

obj = MyClass()
obj.hello()

Output:

Calling method hello
Hello from MyClass

Conclusion

Python decorators are a powerful tool that helps in extending the functionality of functions and methods without modifying their original implementation. They improve code reusability, readability, and maintainability. Whether used for logging, timing execution, authentication, or enforcing access control, decorators are an essential feature for efficient Python programming.

FAQ

1. What is a Python decorator?

A decorator in Python is a function that modifies the behavior of another function or class method. It is typically used to wrap another function to extend its behavior without modifying its code.

2. How do decorators work?

Decorators take a function as an argument, add some functionality to it, and return a modified function. They are often implemented using function closures or functools.wraps to maintain metadata.

3. What is the syntax for using a decorator?

You use the @decorator_name syntax before defining a function:

def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Output:

Before function call
Hello!
After function call

4. Can a decorator take arguments?

Yes, you can make a decorator accept arguments by nesting another function inside it:

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!")

greet()

Output:

Hello!
Hello!
Hello!

5. How can I apply multiple decorators to a function?

You can stack multiple decorators by listing them in order, top to bottom:

def uppercase(func):
    def wrapper():
        return func().upper()
    return wrapper

def exclaim(func):
    def wrapper():
        return func() + "!"
    return wrapper

@uppercase
@exclaim
def greet():
    return "hello"

print(greet())  # Output: HELLO!

6. What is functools.wraps and why is it needed?

The functools.wraps decorator helps preserve the original function’s metadata when wrapping it with another function:

from functools import wraps

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

@my_decorator
def say_hello():
    """This function says hello."""
    print("Hello!")

print(say_hello.__name__)  # Output: say_hello
print(say_hello.__doc__)   # Output: This function says hello.

Without @wraps, say_hello.__name__ would return "wrapper" instead of "say_hello".

7. Can I use decorators on class methods?

Yes, decorators work on class methods in the same way as functions:

class Greeter:
    @staticmethod
    def greet():
        print("Hello!")

    @classmethod
    def class_greet(cls):
        print(f"Hello from {cls.__name__}!")

Greeter.greet()
Greeter.class_greet()

8. What are some common built-in decorators in Python?

Python provides several built-in decorators, such as:

  • @staticmethod – Defines a static method in a class.
  • @classmethod – Defines a class method that takes cls as the first argument.
  • @property – Turns a method into a read-only property.
  • @functools.lru_cache – Caches the results of a function to improve performance.

9. Can decorators be used with arguments in functions?

Yes, decorators work with functions that accept arguments by using *args and **kwargs:

def debug(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args} and {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@debug
def add(x, y):
    return x + y

print(add(3, 5))

Output:

Calling add with (3, 5) and {}
8

10. When should I use decorators in Python?

Decorators are useful for:

  • Logging function calls
  • Timing function execution
  • Access control (e.g., user authentication)
  • Caching results to optimize performance
  • Code reuse by separating concerns
Anastasios Antoniadis
Find me on
Latest posts by Anastasios Antoniadis (see all)

Leave a Comment