DEV Community

MarKSmaN98
MarKSmaN98

Posted on • Updated on

Python Decorators

Python is a high-level programming language that offers a lot of flexibility to programmers. One of the features that make Python such a versatile language is decorators. Decorators are functions that modify other functions or classes, and they provide a way to add functionality to your code without changing the original code.

In this blog post, we will explore what Python decorators are, how they work, and some common use cases.

What are Python decorators?

In Python, a decorator is a function or class that takes a function as its argument and returns a new function or class, respectively. The new function or class is usually a modified version of the original function that adds some additional functionality. For the rest of the blog I will only be referring to function decorators, however please note that class decorators exist, can be called in the same way, and can extend class functionality without modifying the class. The decorator function is called before the original function and can modify the inputs, outputs, or behavior of the original function.

The syntax for a decorator is as follows:

@decorator_function
def my_function():
    pass

Enter fullscreen mode Exit fullscreen mode

The @decorator_function syntax is known as the decorator syntax. It tells Python to pass the function my_function as an argument to the decorator_function.

Types of Python decorators

There are two types of decorators in Python: function decorators and class decorators.

1. Function decorators

Function decorators are the most common type of decorators in Python. They are used to modify the behavior of a function.

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

@my_decorator
def my_function():
    print("Hello, World!")

my_function()

Enter fullscreen mode Exit fullscreen mode

In this example, my_decorator is a function that takes another function func as an argument and returns a new function wrapper. wrapper adds some additional functionality before and after func is called. The @my_decorator syntax tells Python to pass my_function as an argument to my_decorator and use the resulting function as the new definition of my_function.

2. Class decorators

Class decorators are used to modify the behavior of a class.

def my_class_decorator(cls):
    class NewClass:
        def __init__(self, *args, **kwargs):
            self.obj = cls(*args, **kwargs)

        def __getattr__(self, name):
            return getattr(self.obj, name)

        def __setattr__(self, name, value):
            setattr(self.obj, name, value)

    return NewClass

@my_class_decorator
class MyClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def my_method(self):
        return self.x + self.y

obj = MyClass(3, 4)
print(obj.my_method())

Enter fullscreen mode Exit fullscreen mode

In this example, my_class_decorator is a function that takes a class cls as an argument and returns a new class NewClass. NewClass modifies the behavior of cls by intercepting attribute access and delegation to the original cls object. The @my_class_decorator syntax tells Python to use my_class_decorator to modify the behavior of MyClass.

Uses of Python decorators

Python decorators can be used for a wide range of purposes, including:

1. Logging
Decorators can be used to log the input and output of a function.

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

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

add(3, 4)

Enter fullscreen mode Exit fullscreen mode

In this example, the log decorator is used to log the input and output of the add method. The log decorator takes a function func as an argument and returns a new function wrapper that logs the input and output of func. The wrapper takes any number of positional and keyword arguments by using *args and **kwargs respectively. To use the log decorator simply add '@log' on the line before your function declaration.

2. Timing
Decorators can also be used to time the execution of a function.

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:.2f} seconds to execute.")
        return result
    return wrapper

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

slow_function()

Enter fullscreen mode Exit fullscreen mode

In this example, the timer decorator is used to time the execution of the slow_function. The decorator measures the time taken to execute the function and prints the elapsed time.

3. Authorization
Decorators can be used to restrict access to certain functions or classes based on authorization rules.

def authorized(func):
    def wrapper(*args, **kwargs):
        if is_authorized():
            return func(*args, **kwargs)
        else:
            raise Exception("You are not authorized to access this function.")
    return wrapper

@authorized
def secure_function():
    pass

secure_function()

Enter fullscreen mode Exit fullscreen mode

In this example, the authorized decorator is used to restrict access to the secure_function. The decorator checks if the user is authorized to access the function and raises an exception if the user is not authorized.

4. Caching
Decorators can be used to cache the results of expensive function calls.

def cache(func):
    results = {}
    def wrapper(*args):
        if args in results:
            print(f"Returning cached result for {args}")
            return results[args]
        result = func(*args)
        results[args] = result
        print(f"Caching result for {args}")
        return result
    return wrapper
Enter fullscreen mode Exit fullscreen mode

In this example, cache uses a wrapper to capture and store the results of func. The wrapper function first checks if the function has already been called with the same arguments by checking if the args are already in the results dictionary. If the args are in the results dictionary, the wrapper function returns the cached result and logs a message indicating that the result is being returned from the cache.

This is only a short list of what can be done with python decorators! The sky is truly the limit when you can call a function to modify another one. Some other useful implementations of decorators might include retry, validation, debugging, etc. If you're curious to dive into a deep but incredibly useful topic maybe try starting your journey here.

Top comments (2)

Collapse
 
jj profile image
Juan Julián Merelo Guervós

Just a nit here. You say:

In Python, a decorator is a function that takes another function as its argument and returns a new function

However, a bit later you also talk about decorators for classes, which are certainly not a function. Possibly what you mean in the sentence above is that in general, decorators apply only to functions; however, Python extends the concept of decorator to classes too.

Collapse
 
marksman98 profile image
MarKSmaN98

Yep! My bad, and thank you for taking the time to point it out. The details are important!