JD
Jane Doe
A passionate software engineer exploring the depths of Python and its elegant features.

Python Decorators Explained

Published: October 26, 2023

Decorators in Python are a powerful and versatile feature that allows you to modify or enhance functions or methods in a clean and readable way. They are a form of metaprogramming, meaning they are code that manipulates other code.

What is a Decorator?

At its core, a decorator is a callable that takes a function as an argument and returns a new function. This new function typically wraps the original function, adding some behavior before or after the original function is executed, or even replacing it entirely.

The syntax for applying a decorator is the @decorator_name syntax placed directly above the function definition.

A Simple Example

Let's start with a basic decorator that logs when a function is called:


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

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

greet("Alice")
        

When you run this code, you'll see output like:


Calling function: greet
Hello, Alice!
Function greet finished.
        

How it Works

The @log_function_call syntax is syntactic sugar for the following:


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

greet = log_function_call(greet)
        

The log_function_call decorator takes the greet function, creates a wrapper function that adds logging, and then reassigns the name greet to this new wrapper function. So, when you call greet("Alice"), you are actually calling the wrapper function.

Key Concepts

Using functools.wraps


import functools

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

@log_function_call_with_wraps
def say_goodbye(name):
    """A simple function to say goodbye."""
    print(f"Goodbye, {name}!")

say_goodbye("Bob")
print(f"Docstring of say_goodbye: {say_goodbye.__doc__}")
print(f"Name of say_goodbye: {say_goodbye.__name__}")
        

Without @functools.wraps, the docstring and name would belong to the wrapper function.

Practical Use Cases

Decorators with Arguments

You can also create decorators that accept arguments. This requires an extra layer of nesting:


import functools

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def say_whee():
    print("Whee!")

say_whee()
        

This decorator repeat takes an argument num_times and returns another decorator that will apply the function num_times.

Conclusion

Python decorators are a powerful tool for adding reusable functionality to your functions and methods without altering their core logic. By understanding closures and the @ syntax, you can write cleaner, more maintainable, and more expressive Python code.

Tags: Python Decorators Programming Web Development Metaprogramming