top of page

Decorators in Python



In Python, a decorator is a special kind of function that can modify the behavior of another function. Decorators are a powerful tool for adding functionality to functions without changing their source code.


How decorators work

Decorators take another function as an argument and return a new function that usually performs some additional processing before or after the original function is called. This new function can then be used in place of the original function, with the added functionality provided by the decorator.

Here's a simple example of a decorator that prints a message before and after a function is called:

def my_decorator(func):
    def wrapper():
        print("Before the function is called.")
        func()
        print("After the function is called.")
    return wrapper

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

say_hello()

This will output:

Before the function is called.
Hello!
After the function is called.

In this example, my_decorator is a function that takes another function as an argument (func) and returns a new function (wrapper) that prints a message before and after calling func. The @my_decorator syntax is a shorthand way of applying the decorator to the say_hello function.


Common use cases for decorators

There are many use cases for decorators in Python. Here are a few common ones:


Logging

Decorators can be used to log function calls, which can be useful for debugging or performance analysis. Here's an example:

import logging

def log_calls(func):
    def wrapper(*args, **kwargs):
        logging.info("Calling %s with args %s and kwargs %s" % (func.__name__, args, kwargs))
        result = func(*args, **kwargs)
        logging.info("Function %s returned %s" % (func.__name__, result))
        return result
    return wrapper

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

add(2, 3)

This will log the following messages:

INFO:root:Calling add with args (2, 3) and kwargs {}
INFO:root:Function add returned 5


Caching

Decorators can be used to cache the results of expensive function calls, which can improve performance by avoiding redundant computations. Here's an example:

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

@cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

fibonacci(10)

This will return the result of fibonacci(10) (55) and cache the results of previous calls to fibonacci.


Timing


Decorators can be used to time the execution of functions, which can be useful for profiling or optimization. Here's an example:

import time

def time_it(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print("Function %s took %f seconds to execute" % (func.__name__, end_time - start_time))
        return result
    return wrapper

@time_it
def slow_function():
    time.sleep(2)

slow_function()

This will output:

Function slow_function took 2.001466 seconds to execute


Method Chaining


Decorators can be used to implement method chaining in Python. Method chaining is a programming pattern where multiple methods are called on an object in a single statement, with each method returning the object itself. This can make code more concise and readable.

Here's an example:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def set_name(self, name):
        self.name = name
        return self

    def set_age(self, age):
        self.age = age
        return self

    def __str__(self):
        return f"{self.name}, {self.age}"

person = Person("Alice", 30)
person.set_name("Bob").set_age(40)
print(person) # Output: Bob, 40

In this example, the set_name and set_age methods return the Person object itself, which allows them to be chained together in a single statement.


Authentication


Decorators can be used to implement authentication or authorization checks in Python. This can be useful for restricting access to certain parts of an application or API.

Here's an example:

def requires_authentication(func):
    def wrapper(*args, **kwargs):
        # Check if the user is authenticated
        if not is_authenticated():
            raise Exception("User not authenticated")
        return func(*args, **kwargs)
    return wrapper

@requires_authentication
def delete_user(user_id):
    # Delete the user from the database
    pass

In this example, the requires_authentication decorator checks if the user is authenticated before allowing them to call the delete_user function. If the user is not authenticated, an exception is raised.


Memoization


Decorators can be used to implement memoization in Python. Memoization is a programming technique where the results of expensive function calls are cached and returned when the same inputs occur again.

Here's an example:

def memoize(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@memoize
def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10)) # Output: 55

In this example, the memoize decorator caches the results of previous calls to the fibonacci function, which can improve performance when computing large Fibonacci numbers.


Conclusion

Decorators are a powerful tool in Python that can be used for a variety of purposes, including method chaining, authentication, and memorization. By using decorators, you can add functionality to functions in a flexible and composable way, which can make your code more modular and easier to maintain.

bottom of page