Koding Books

Professional, free coding tutorials

Python Decorators

Introduction

A Python decorator is a design pattern that allows you to modify the behaviour of a function or class. It is a function that takes another function as input, adds some functionality, and returns the modified function. Decorators are denoted by the “@” symbol followed by the name of the decorator function.

Decorator use cases

Decorators are a powerful tool in Python and can be used in many different ways. Here are some common use cases for decorators in Python:

  1. Logging: Decorators can be used to log function calls, arguments and return values.
  2. Timing: Decorators can be used to time how long a function takes to execute.
  3. Caching: Decorators can be used to cache the results of a function so that it doesn’t have to be recomputed every time it is called.
  4. Authorization: Decorators can be used to check if a user is authorized to access a particular function or resource.
  5. Validation: Decorators can be used to validate function arguments and return values.
  6. Retry: Decorators can be used to retry a function if it fails due to a temporary error.
  7. Memoization: Decorators can be used to memoize a function, which means that it remembers the results of previous function calls and returns the cached result if the same arguments are passed again.

These are just a few examples of the many use cases for decorators in Python

Example

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Elapsed time: {end_time - start_time}")
        return result
    return wrapper

@timer_decorator
def my_function():
    time.sleep(2)
    print("Function executed")

my_function()

In this example, the timer_decorator function is defined to take a function as input and return a new function that wraps the original function with a timer.

The wrapper function is the new function that is returned by the decorator, and it takes any number of arguments and keyword arguments using the *args and **kwargs syntax. The wrapper function calls the original function, measures the elapsed time, and returns the result. Finally, the @timer_decorator decorator is applied to the my_function function, which adds the timer functionality. When my_function is called, it will print the elapsed time in addition to executing the function.

Chaining decorators

You can combine multiple decorators by applying them one after the other, separated by the @ symbol. Here’s an example of how to chain two decorators:

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("This is my function.")

my_function()

In this example, the decorator1 and decorator2 functions are defined to take a function as input and return a new function that adds some functionality to it.

The wrapper functions are the new functions returned by the decorators, and they call the original function func and add the decorator functionality before and after it. Finally, the @decorator1 and @decorator2 decorators are applied to the my_function function, which chains the two decorators together. When my_function is called, it will print “Decorator 1”, then “Decorator 2”, and then execute the function and print “This is my function.”.

Note that the order of the decorators matters. In this example, decorator1 is applied first to execute it before decorator2. If you reverse the order of the decorators, the output will be different

Class and function decorators

In Python, both class decorators and function decorators are used to modify the behaviour of functions or classes. However, there are some differences between the two:

  1. Input: A function decorator takes a function as input, while a class decorator takes a class as input.
  2. Output: A function decorator returns a function, while a class decorator returns a class.
  3. Scope: A function decorator can only modify the behaviour of the function it is applied to, while a class decorator can modify the behaviour of all class instances.
  4. Syntax: A function decorator is denoted by the “@” symbol followed by the name of the decorator function, while a class decorator is denoted by the “@” symbol followed by the name of the decorator class.

Here’s an example of a class decorator that adds a greeting to all instances of a class:

class GreetingDecorator:
    def __init__(self, cls):
        self.cls = cls

    def __call__(self, *args, **kwargs):
        obj = self.cls(*args, **kwargs)
        print("Hello!")
        return obj

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

my_obj = MyClass(10)
print(my_obj.x)

In this example, the GreetingDecorator class is defined to take a class as input and return a new class that adds a greeting to all instances of the original class. The __call__ method is called when the decorated class is instantiated, and it creates a new instance of the original class and adds the greeting before returning it. Finally, the @GreetingDecorator decorator is applied to the MyClass class, which adds the greeting functionality to all instances of the class. When MyClass is instantiated, it will print “Hello!” before creating the instance and returning it.

Note that the __call__ method is used to make the decorator class callable, which allows it to be used as a function decorator

Decorators and wrappers

In Python, a decorator is a function that takes another function as input, modifies it in some way, and returns the modified function. A wrapper function is a function that takes another function as input, calls it, and returns the result. The main difference between a decorator and a wrapper function is that a decorator returns a new function that has additional functionality, while a wrapper function simply calls the original function and returns the result.

Here’s an example of a decorator that adds a greeting to a function:

def greeting_decorator(func):
    def wrapper():
        print("Hello!")
        func()
        print("Goodbye!")
    return wrapper

@greeting_decorator
def my_function():
    print("This is my function.")

my_function()

In this example, the greeting_decorator function is a decorator that takes a function as input and returns a new function that adds a greeting before and after the original function is called. The wrapper function is the new function that is returned by the decorator, and it calls the original function func and adds the greeting before and after it. Finally, the @greeting_decorator decorator is applied to the my_function function, which adds the greeting functionality to it. When my_function is called, it will print “Hello!”, execute the function, and then print “Goodbye!”.

Here’s an example of a wrapper function that adds a greeting to a function:

def greeting_wrapper(func):
    def wrapped_function():
        print("Hello!")
        result = func()
        print("Goodbye!")
        return result
    return wrapped_function

def my_function():
    print("This is my function.")

my_function = greeting_wrapper(my_function)

my_function()

In this example, the greeting_wrapper function is a wrapper function that takes a function as input and returns a new function that adds a greeting before and after the original function is called. The wrapped_function function is the new function that is returned by the wrapper, and it calls the original function func and adds the greeting before and after it. Finally, the my_function function is wrapped by calling greeting_wrapper(my_function), which returns the wrapped function and assigns it to my_function. When my_function is called, it will print “Hello!”, execute the function, and then print “Goodbye!”.

Decorators and class decorators

In Python, a decorator is a function that takes another function as input, modifies it in some way, and returns the modified function. A class decorator is a function that takes a class as input, modifies it in some way, and returns the modified class. The main difference between a decorator and a class decorator is that a decorator modifies a function, while a class decorator modifies a class.

Here’s an example of a decorator that adds a greeting to a function:

def greeting_decorator(func):
    def wrapper():
        print("Hello!")
        func()
        print("Goodbye!")
    return wrapper

@greeting_decorator
def my_function():
    print("This is my function.")

my_function()

In this example, the greeting_decorator function is a decorator that takes a function as input and returns a new function that adds a greeting before and after the original function is called. The wrapper function is the new function that is returned by the decorator, and it calls the original function func and adds the greeting before and after it. Finally, the @greeting_decorator decorator is applied to the my_function function, which adds the greeting functionality to it. When my_function is called, it will print “Hello!”, execute the function, and then print “Goodbye!”.

Here’s an example of a class decorator that adds a greeting to a class:

def greeting_decorator(cls):
    class WrappedClass:
        def __init__(self, *args, **kwargs):
            self.wrapped_instance = cls(*args, **kwargs)

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

        def greet(self):
            print("Hello!")

    return WrappedClass

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

my_obj = MyClass(10)
my_obj.greet()
print(my_obj.x)

In this example, the greeting_decorator function is a class decorator that takes a class as input and returns a new class that adds a greeting method to the original class. The WrappedClass class is the new class that is returned by the decorator, and it wraps the original class cls by creating a new instance of it and adding a greet method that prints “Hello!”. Finally, the @greeting_decorator decorator is applied to the MyClass class, which adds the greeting functionality to it. When MyClass is instantiated, it will create an instance of the wrapped class WrappedClass, which has the greet method that prints “Hello!”.

Decorating instance methods

def greeting_decorator(func):
    def wrapper(self):
        print("Hello!")
        func(self)
        print("Goodbye!")
    return wrapper

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

    @greeting_decorator
    def my_method(self):
        print("This is my method. x =", self.x)

my_obj = MyClass(10)
my_obj.my_method()

In this example, the greeting_decorator function is a decorator that takes an instance method as input and returns a new instance method that adds a greeting before and after the original method is called. The wrapper function is the new function that is returned by the decorator, and it calls the original method func and adds the greeting before and after it. Finally, the @greeting_decorator decorator is applied to the my_method method of the MyClass class, which adds the greeting functionality to it. When my_obj.my_method() is called, it will print “Hello!”, execute the original method, and then print “Goodbye!”.

Note that the wrapper function takes a self argument, which represents the instance of the class that the method is bound to. This is necessary because instance methods are bound to the instance of the class rather than the class itself. The wrapper function calls the original method func with the self argument to ensure that the method is executed with the correct context.

Meta Data

functools.wraps function is a decorator that is used to preserve the metadata of a function when it is wrapped by another function. When a function is wrapped by another function, the metadata of the original function (such as its name, docstring, and parameter list) can be lost, which can make debugging and introspection more difficult. The functools.wraps decorator is used to solve this problem by copying the metadata of the original function to the wrapper function.

Here’s an example of how to use functools.wraps to preserve the metadata of a function:

from functools import wraps

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

@my_decorator
def my_function(x, y):
    """This is my function."""
    return x + y

print(my_function(1, 2))
print(my_function.__name__)
print(my_function.__doc__)

In this example, the my_decorator function is a decorator that takes a function as input and returns a new function that adds some functionality to it. The wrapper function is the new function returned by the decorator, and it calls the original function func and adds the decorator functionality before and after it. The @wraps(func) decorator is applied to the wrapper function, which copies the metadata of the original function func to the wrapper function. Finally, the @my_decorator decorator is applied to the my_function function, which adds the decorator functionality.

When my_function is called, it will print “Before the function is called.”, execute the function, print “After the function is called.”, and return the result. The __name__ and __doc__ attributes of my_function will also be preserved, thanks to the @wraps(func) decorator.

The Last Byte…

Decorators are a powerful feature of Python that allow you to modify the behavior of functions and classes in a flexible and reusable way. They can be used to add functionality to existing code, such as logging, caching, or authentication, without modifying the original code.

Decorators can also be used to implement cross-cutting concerns, such as error handling or performance monitoring, that apply to multiple functions or classes.

With the functools.wraps decorator, you can preserve the metadata of the original function or class, making debugging and introspection easier. By mastering decorators, you can write more concise, modular, and maintainable code in Python.

Ali Kayani

https://www.linkedin.com/in/ali-kayani-silvercoder007/

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

How E-Trading is Revolutionising the Fixed Income Market

[ C++ ] Challenge | Variable Sized Arrays

White hat hacking using Python

Accumulation/Distribution Line indicator