Python Decorators


In Python, decorators are a powerful and elegant way to modify the behavior of functions or methods. They allow you to wrap a function or method with additional functionality without altering its core logic. Decorators are commonly used for logging, authentication, performance measurement, and many other tasks, making them an essential tool in a Python developer's toolkit.

In this blog post, we’ll break down what Python decorators are, how they work, and provide practical examples to help you understand how to use them effectively.


Table of Contents

  1. What is a Python Decorator?
  2. How Python Decorators Work
  3. Creating Your Own Decorators
  4. Using Built-in Python Decorators
  5. Chaining Multiple Decorators
  6. Decorators with Arguments
  7. Common Use Cases of Python Decorators

What is a Python Decorator?

A decorator in Python is a design pattern that allows you to add functionality to an existing function or method. It is typically used to modify or enhance a function’s behavior without changing the function itself. In Python, decorators are implemented as functions that take another function as an argument and return a new function that usually extends or alters the behavior of the original function.

Decorators are often used to modify or extend the behavior of functions or methods in a reusable and readable manner. The most common use of decorators is in adding functionality like logging, validation, or access control to functions or methods in a clean and maintainable way.

Example of a Simple Decorator

Here’s a basic example to illustrate how decorators work:

def decorator_function(original_function):
    def wrapper_function():
        print("Wrapper executed this before {}".format(original_function.__name__))
        return original_function()
    return wrapper_function

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

display()

Output:

Wrapper executed this before display
Display function executed!

In this example:

  • The decorator_function takes a function (original_function) as an argument.
  • The wrapper_function wraps the original_function and adds behavior (in this case, a print statement) before calling it.
  • The @decorator_function syntax is a shorthand for applying the decorator to the display function, which means display is passed to decorator_function.

How Python Decorators Work

Python decorators are implemented using higher-order functions. A higher-order function is a function that takes another function as an argument or returns a function as its result. Decorators are a specific type of higher-order function that are used to modify or enhance other functions.

The Basic Workflow of a Decorator

  1. A function (the decorator) takes another function as input.
  2. The decorator defines a wrapper function, which adds some functionality and calls the original function.
  3. The decorator returns the wrapper function as a new function.
  4. The original function is replaced by the wrapper function when using the @decorator syntax.

Creating Your Own Decorators

Creating your own decorators in Python is straightforward. A decorator is essentially a function that returns another function. Here’s a simple step-by-step example of creating a custom decorator.

Step-by-Step Example: A Simple Logging Decorator

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Function {func.__name__} is called with arguments {args} and keyword arguments {kwargs}")
        result = func(*args, **kwargs)
        return result
    return wrapper

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

add(2, 3)

Output:

Function add is called with arguments (2, 3) and keyword arguments {}

In this example:

  • The log_function_call decorator prints the function name, arguments, and keyword arguments whenever the decorated function is called.
  • @log_function_call is applied to the add function, which logs the function call details every time it is invoked.

Using Built-in Python Decorators

Python provides several built-in decorators that can be used to add common functionality to your functions or methods. Some of the most commonly used built-in decorators include:

  1. @staticmethod: Used to define a static method in a class, meaning it doesn’t depend on class or instance state.

    class MyClass:
        @staticmethod
        def greet(name):
            print(f"Hello, {name}!")
    
    MyClass.greet("John")
    
  2. @classmethod: Used to define a class method that operates on the class itself, rather than on an instance of the class.
    class MyClass:
        @classmethod
        def greet(cls, name):
            print(f"Hello from class, {name}!")
    
    MyClass.greet("John")
    
  3. @property: Used to define a method as a property, which can be accessed like an attribute.
    class Circle:
        def __init__(self, radius):
            self._radius = radius
    
        @property
        def radius(self):
            return self._radius
    
        @radius.setter
        def radius(self, value):
            if value < 0:
                raise ValueError("Radius cannot be negative")
            self._radius = value
    
    c = Circle(5)
    print(c.radius)
    

Chaining Multiple Decorators

You can apply multiple decorators to a function, which allows you to add several layers of functionality. The decorators are applied in the order from bottom to top.

Example of Chaining Decorators

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

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

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

greet()

Output:

Decorator One
Decorator Two
Hello!

In this example:

  • greet() is decorated with decorator_two first and then with decorator_one.
  • When greet() is called, the decorators are executed in reverse order.

Decorators with Arguments

Sometimes, you may need to pass arguments to a decorator. This requires an extra level of function nesting. Here’s how you can create a decorator that accepts arguments.

Example: A Decorator with Arguments

def repeat_decorator(repeats):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(repeats):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat_decorator(3)
def say_hello():
    print("Hello!")

say_hello()

Output:

Hello!
Hello!
Hello!

In this example:

  • The repeat_decorator takes an argument (repeats) and repeats the decorated function's execution that many times.
  • When calling say_hello(), it prints "Hello!" three times.

Common Use Cases of Python Decorators

Python decorators are used in a wide range of real-world scenarios. Some common use cases include:

  1. Logging: Adding logging functionality to track when functions are called and their arguments.
  2. Access Control: Using decorators to enforce authentication or permission checks on functions (e.g., in web frameworks).
  3. Memoization/Caching: Caching the results of expensive function calls to improve performance.
  4. Timing: Measuring how long a function takes to execute and logging the duration.
  5. Validation: Checking inputs before running the function to ensure they are valid (e.g., input validation).