Decorators in python

Decorators in python

In Python, a decorator is a function that modifies the behavior of another function without changing its source code. It is used extensively to add functionality to an existing function or class, such as logging, timing, authentication, or synchronization, among other things.

One major advantage of using decorators in Python is code reusability. Instead of adding the same functionality to multiple functions, you can write a decorator once and apply it to any function you want. This also helps keep your code base clean and organized, as you don't have to repeat the same code in multiple places.

Another advantage is that decorators allow you to separate concerns and add features or behaviors incrementally without modifying the underlying function's code. This can make your code more flexible, maintainable, and adaptable to change over time.

In other programming languages like Java or C#, similar functionality can be achieved using 'annotations' or 'attributes', respectively.

Why is it good to use decorators in Python?

  • Simpler code: By using a decorator, you can avoid duplicating code in multiple places.

  • More extensible: Decorators enables you to add features to an existing function, such as adding caching, logging, or rate-limiting, without modifying the underlying implementation.

  • Code Reusability: Oftentimes, several functions might require similar logic to be executed either before or after the execution of the actual function code. A decorator can provide this generic "wrapper" logic by wrapping the main function.

  • Authentication/Authorization: Decorators can be used to verify user permission levels before allowing access to restricted portions of the application.

  • Performance Optimization: Decorators can be used to cache the results of a function in memory.

  • Debugging: Logging, debug prints and other error-handling checks can be added as decorators

Syntax

@decorator
def my_function():
    # function implementation

In this syntax, decorator is a callable object that takes a function as input and returns a new function as output. The new function behaves differently from the original function due to the behavior added by the decorator.

Here are three examples of using decorators in Python:

  • Simple decorator example, which adds some functionality to a function:
def my_wrapper(func):
    def wrapper():
        print("Before the main function executed")
        func()
        print("After the main function executed")

    return wrapper()

@my_wrapper
def my_function():
    print("function was executed!")

output:

  • Decorator with arguments example:
def repeat(num):
    def my_decorator(func):
        def wrapper():
            for i in range(num):
                func()

        return wrapper

    return my_decorator

@repeat(3)
def say_hello():
    print("Hello World")

say_hello()

output:

  • Class decorator example:
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)

@CountCalls
def say_bye():
    print("Bye Bye")

say_bye()
say_bye()
say_bye()

output:

Property Decerator

In Python, @property is a built-in decorator that allows you to define a method that can be accessed as an attribute, making it easier to access and modify the values of private attributes.

When you use the @property decorator on a function, it is converted into a read-only property. It creates an object that can be used as a normal attribute with the additional functionality of underlying code for getters, setters, and deleters.

here is an example:

class Car:
    def __init__(self, brand: str, model: int):
        self.brand = brand
        self.model = model

    @property
    def get_brand_capital(self) -> str:
        return self.brand.capitalize()


car1 = Car(brand="audi", model=2007)
car2 = Car(brand="ford", model=2023)

print(car1.get_brand_capital)  # Not print(car1.get_brand_capital())
print(car2.get_brand_capital)  # print(car2.get_brand_capital())

output:

login_required in Django

here is an example of using decorators in Django:

from django.contrib.auth.decorators import login_required
from django.shortcuts import render

@login_required
def my_view(request):
    """A view that can only be accessed by authenticated users"""
    return render(request, 'myapp/my_template.html')

In the above example, we are using the @login_required decorator to ensure that only authenticated users can access the my_view function. If a user tries to access this view while not logged in, they will be redirected to the login page.

The @login_required decorator is a built-in decorator provided by Django that simplifies the process of authenticating users. It checks whether the user is authenticated and, if not, redirects them to the login page.

You can also create your custom decorators in Django to add additional functionality or to modify the behavior of views based on certain conditions.

thank you.