Thursday, November 2, 2023

Python Decorators Unwrapped: Elevate Your Code with Powerful Function Enhancements!🚀✨

  • Python decorators are a powerful and essential concept in Python programming. They allow you to modify or enhance the behavior of functions or methods without changing their code. 
  • In this blog post, we'll explore the fundamentals of Python decorators and provide you with practical examples to help you understand how to create and use them effectively.

When to Use a Decorator in Python

  • A few good examples are when you want to add logging, test performance, perform caching, verify permissions, and so on.
  • You can also use one when you need to run the same code on multiple functions. This avoids you writing duplicating code.

Prerequisites for learning decorators

  • Before we learn about decorators, we need to understand a few important concepts related to Python functions. Also, remember that everything in Python is an object, even functions are objects.
1. Nested Functions:
  • Decorators often involve the use of nested functions. A nested function is a function defined within another function. 
  • This concept is crucial for understanding how decorators work because decorators are essentially functions within functions 📦.

def outer(x): def inner(y): return x + y return inner add_five = outer(5) result = add_five(6) print(result) # Output: 11
2. Passing Functions as Arguments:
  • In Python, functions are first-class objects, which means you can pass functions as arguments to other functions. 
  • This is a fundamental concept for decorators because decorators take functions as arguments to modify their behavior  🔄.

def add(x, y):
return x + y def calculate(func, x, y): return func(x, y) result = calculate(add, 4, 6) print(result) # prints 10
3. Returning a Function as a Value:
  • Functions can be returned as values from other functions, which is a key aspect of decorators. The returned function typically extends or modifies the behavior of the original function 🎁

def greeting(name): def hello(): return "Hello, " + name + "!" return hello greet = greeting("Atlantis") print(greet()) # prints "Hello, Atlantis!" # Output: Hello, Atlantis!

Understanding Decorators

  • Now, let's delve into the heart of decorators: In Python, functions are first-class objects, meaning they can be passed as arguments to other functions and returned as values from other functions. This characteristic forms the foundation for decorators.
  • A decorator, in its simplest form, is a function that takes another function as an argument and returns a new function that usually extends or modifies the behavior of the original function. 🖌️
Creating a Decorator
  • As mentioned earlier, A Python decorator is a function that takes in a function and returns it by adding some functionality.
  • In fact, any object which implements the special __call__() method is termed callable. So, in the most basic sense, a decorator is a callable that returns a callable.
  • Basically, a decorator takes in a function, adds some functionality and returns it.

def make_pretty(func): def inner(): print("I got decorated") func() return inner
def ordinary(): print("I am ordinary")
# Output: I am ordinary
  • Here, we have created two functions:
    • ordinary() : that prints "I am ordinary"
    • make_pretty()  that takes a function as its argument and has a nested function named inner(), and returns the inner function.
  • We are calling the ordinary() function normally, so we get the output "I am ordinary". Now, let's call it using the decorator function.

def make_pretty(func): # define the inner function
def inner(): # add some additional behavior to decorated function print("I got decorated") # call original function func() # return the inner function return inner # define ordinary function def ordinary(): print("I am ordinary") # decorate the ordinary function decorated_func = make_pretty(ordinary) # call the decorated function decorated_func()

#Output I got decorated I am ordinary
In the example shown above, make_pretty() is a decorator. Notice the code.

decorated_func = make_pretty(ordinary)
  • We are now passing the ordinary() function as the argument to the make_pretty().
  • The make_pretty() function returns the inner function, and it is now assigned to the decorated_func variable.

decorated_func()
  • Here, we are actually calling the inner() function, where we are printing
@ Symbol With Decorator
  • Instead of assigning the function call to a variable, Python provides a much more elegant way to achieve this functionality using the @ symbol. 
  • For example

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner

@make_pretty
def ordinary():
    print("I am ordinary")

ordinary()  

#Output
I got decorated
I am ordinary
  • Here, the ordinary() function is decorated with the make_pretty() decorator using the @make_pretty syntax, which is equivalent to calling ordinary = make_pretty(ordinary).

Decorating Functions with Parameters

  • The above decorator was simple and it only worked with functions that did not have any parameters. What if we had functions that took in parameters like:
def divide(a, b):
    return a/b
  • This function has two parameters, a and b. We know it will give an error if we pass in b as 0.
Now let's make a decorator to check for this case that will cause the error.
def smart_divide(func):
    def inner(a, b):
        print("I am going to divide", a, "and", b)
        if b == 0:
            print("Whoops! cannot divide")
            return
        return func(a, b)
    return inner

@smart_divide
def divide(a, b):
    print(a/b)

divide(2,5)
divide(2,0)
Output
I am going to divide 2 and 5
0.4
I am going to divide 2 and 0
Whoops! cannot divide
  • Here, when we call the divide() function with the arguments (2,5), the inner() function defined in the smart_divide() decorator is called instead.
  • This inner() function calls the original divide() function with the arguments 2 and 5 and returns the result, which is 0.4.
  • Similarly, When we call the divide() function with the arguments (2,0), the inner() function checks that b is equal to 0 and prints an error message before returning None.
Let's create a decorator that measures the time taken by a function to execute. Here's how it's done
import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time} seconds to execute.")
        return result
    return wrapper

@timing_decorator
def some_function():
    # Simulate a time-consuming task
    time.sleep(2)

some_function()
  • In this example:We define a decorator timing_decorator that takes a function as an argument, records the start time, calls the original function, records the end time, and prints the execution time.
  • We apply this decorator to the some_function using the @ symbol.

Chaining Decorators in Python

  • You can apply multiple decorators to a single function, creating a chain of decorators. The order of decoration matters, as decorators are applied from top to bottom. Here's an example of chaining decorators:
def star(func):
    def inner(*args, **kwargs):
        print("*" * 15)
        func(*args, **kwargs)
        print("*" * 15)
    return inner

def percent(func):
    def inner(*args, **kwargs):
        print("%" * 15)
        func(*args, **kwargs)
        print("%" * 15)
    return inner

@star
@percent
def printer(msg):
    print(msg)

printer("Hello")
Output:
***************
%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%
***************
The above syntax of,
@star @percent def printer(msg): print(msg)
is equivalent to:
def printer(msg): print(msg) printer = star(percent(printer))
The order in which we chain decorators matter. If we had reversed the order as,
@percent @star def printer(msg): print(msg)
The output would be:
%%%%%%%%%%%%%%% *************** Hello *************** %%%%%%%%%%%%%%%

Decorating Functions with Arguments & Return Value

  • Decorators can also accept arguments, allowing you to customize their behavior. Let's explore an example in detail:
def repeat(num): def decorator(func): def wrapper(*args, **kwargs): for _ in range(num): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def say_hello(name): print(f"Hello, {name}") say_hello("Bob")
Output:
Hello, Bob Hello, Bob Hello, Bob
  • In this case:We define a decorator repeat that takes an argument num, specifying how many times the function should be executed.
  • The decorator function returns the wrapper function, which repeats the execution of the decorated function.
  • We apply the @repeat(3) decorator to the say_hello function, causing it to print "Hello, Bob" three times. 🔄

  • Now, let's discuss how decorated functions can return values. In the previous examples, the decorated functions had no return value. However, decorated functions can return values as well. Here's an example:
def multiply_by(factor): def decorator(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result * factor return wrapper return decorator @multiply_by(2) def add_numbers(a, b): return a + b result = add_numbers(3, 4) print(result)
  • In this example: We define a multiply_by decorator that takes an argument factor.
  • The decorated function add_numbers returns the sum of two numbers.
  • The wrapper function, inside the decorator, captures the result from the decorated function and multiplies it by the factor.
  • When we call add_numbers(3, 4), the decorated function returns 7, and the multiply_by(2) decorator modifies the result to 14🌟.
Exercise_1 - Library Access Control System

Exercise_2 - Task Management System

    Conclusion

    • Python decorators are a powerful feature that can simplify code and enhance the functionality of your programs. By creating and using decorators, you can separate concerns, reuse code, and keep your codebase clean and readable. 
    • Whether you're working on web development, data analysis, or any other Python project, decorators can be a valuable tool in your programming arsenal.
    • With a solid understanding of the prerequisites and examples provided in this post, you're well-equipped to leverage decorators effectively in your Python projects. 
    • These concepts not only help you understand how decorators work but also enable you to create custom decorators tailored to your specific needs. Decorators are a versatile and elegant way to enhance the functionality of your Python applications, including the ability to return values from decorated functions. 💡🚀

    You may also like

    Kubernetes Microservices
    Python AI/ML
    Spring Framework Spring Boot
    Core Java Java Coding Question
    Maven AWS