Understanding Python Decorators: By Planning A Wedding!

Hello, Pythonistas🙋‍♀️ welcome back. Today we’ll discuss decorators✨ in python

In this post, we’re going to embark on a unique adventure🧗‍♀️ together: we’re going to step into the shoes 👞 of a wedding planner. Yes, you heard👂(read) it right!

Just like a wedding💒 planner magically transforms a plain hall or garden🏡 into a stunning venue, they do so by adding decorations✨ without altering its fundamental structure.

Similarly, decorators in Python work their magic by enhancing the functionality of functions or classes. They achieve this without modifying the core implementation of the functions or classes.

Let’s get started…

Previous post’s challenge’s solution

Here’s the solution 🧪 to the challenge provided in the last post:

import os
def file_search(directory, extension):
    """ Generator function that yields files with a given extension in the specified directory.
    Args:
        directory (str): Directory path to search for files.
        extension (str): File extension to search for.
    Yields:
        str: File name with the specified extension.
    Raises:
        FileNotFoundError: If the specified directory does not exist."""
    # checking if there is any such directory
    try:
        # getting all files and folders in the given directory
        all_files = os.listdir(directory)
        # gets the files/folders one by one
        for file in all_files:
            # yields the file only when it has the given extension
            if file.endswith(f".{extension}"):
                yield file
    # when directory doesn't exist
    except FileNotFoundError:
        print("This directory doesn't exist!")
directory = input("Enter the directory path: ")
extension = input("Enter the file extension: ")
 
# Iterate over the matching files
files = file_search(directory, extension)
for file in files:
    print(file)

Please read the comments in the code to understand it clearly.👍

If you still have any doubts ask them in the comment section below.👇

What are decorators?

Decorators in Python are special✨ functions or classes that modify🛠️ the behavior of other functions or classes using a concise🤏 syntax.

The decorator takes up a function or a class and returns a new🌟 function or class usually with modified behavior.

This new function or class can then be used in place of the original, allowing you to add extra➕ features or change the behavior of the decorated entity.

For example, you as a wedding💒 planner decorate a garden🏡 to make it beautiful and apt for a marriage👰 function.

However, you don’t change the core structure of the garden🏡; you simply enhance it with additional💐🎊🎉 functionalities.

As a result, the garden🏡 can be used for other purposes as well, as it retains its original structure.

For instance, let’s see the example of the @staticmethod decorator. It makes the method independent🦅 of any instance and allows it to be directly used.

class MathUtils:
    @staticmethod
    def add_numbers(a, b):
        return a + b
result = MathUtils.add_numbers(10, 6)
print("Result:", result) #16

Here as you can see the add() method can be used directly🦅 without the need of any instance. This is an addition➕ to its functionality.

However, it can be used with an instance too. As in this case, there is no modification just addition.

obj = MathUtils
result = obj.add_numbers(10, 6)
print("Result:", result) #16

Note: you can use more than one decorator(s) on a function known as chaining of decorators. Also, one decorator can be used to decorate more than one functions.

How to create your own decorators

So, you know what are decorators✨. Let’s see how you can create your own decorators with ease⛸️.

You need to know these 3✌️☝️ things to be able to create your own decorators:

  1. A function can take a function as a parameter.
  2. A function can have another function(s) inside it.
  3. A function can return a function.

As we know decorator💐 is a function, so let’s define our decorator function in the first step which takes another function as argument.

Any business🏢 has some terms and conditions. You want to simply print them anytime a client has booked🤝 you.

def t_and_c(fx):
    pass

Next, let’s add functionality to this input function using a nested function.

We will first print terms and condition and will then call the function.

def t_and_c(fx):
    def wrapper():
        print("1. Non refundable")
        print("2. Insurance of 60\% damage done before")
        fx()

Now, let’s return this new🌟 function. And use it as we use decorators.

def t_and_c(fx):
    def wrapper():
        print("1. Non refundable")
        print("2. Insurance of 60% damage done before")
        fx()
    return wrapper
@t_and_c
def booking():
    print("Thanks for booking us!")
booking()

There we go we just made our own decorator.

Decorators on functions with parameters

But, what if the function took some arguments when you call📱 it?

Like:

@t_and_c
def booking(fee):
    print("Thanks for booking us!")
    print(f"You'll need to pay ${fee}")

In this case, our @t_and_c won’t work. One way to deal🤝 with it can be to give one☝️ argument each to wrapper() and to fx().

But, what if another function that uses this decorator takes 0 or more than one argument(s)? 🤷

Well, in this case, we would need to use the *args and the **kwargs inside both wrapper() and fx():

def t_and_c(fx):
    def wrapper(*args, **kwargs):
        print("1. Non refundable")
        print("2. Insurance of 60% damage done before")
        fx(*args, **kwargs)
    return wrapper
@t_and_c
def booking(fee):
    print("Thanks for booking us!")
    print(f"You'll need to pay ${fee}")
booking(1000)

Decorators with parameters

Now what if the decorator itself needs to take up an input other than the function?🤔🤷 This can be needed when the functioning inside the decorator✨ changes based on the function on which it is being used.

One of the most common ways to do this is using the factory🏭 function. It is nothing but nesting the decorator inside another function that will take the argument and return the decorator.

Say our t_and_c() decorator takes an extra condition📝 this condition would depend on the function which uses it or say the function it decorates.

def t_and_c_factory(condition):
    def t_and_c(fx):
        def wrapper(*args, **kwargs):
            print("1. Non refundable")
            print("2. Insurance of 60% damage done before")
            print(f"3. Additional condition: {condition}")
            fx(*args, **kwargs)
        return wrapper
    return t_and_c
@t_and_c_factory("No smoking in the garden")
def booking(fee):
    print("Thanks for booking us!")
    print(f"You'll need to pay ${fee}")
    
booking(1000)

Another way of Using decorator function

We saw that we can use decorators💐 like this using the @ symbol:

@t_and_c
def booking(fee):
    print("Thanks for booking us!")
    print(f"You'll need to pay ${fee}")

But you can also use decorator as a normal function like this:

def booking(fee):
    print("Thanks for booking us!")
    print(f"You'll need to pay ${fee}")
t_and_c(booking)(1000)

Here the t_and_c function takes the booking function and the wrapper function inside t_and_c function takes 1000 as argument to pass it to the booking later.

There can be more than one way to use functions like this. But the @ method wins🎊🎉 over all of them.

Real-world🗺️ examples

Now, we saw what are decorators and how you can make one. But aren’t you really interested🕵️‍♀️ in knowing what are some real-world🗺️ applications of decorators?

Logging

When you go to a grocery🛍️ store all the items you bought are scanned one by one, for getting all information ℹ️ about them.

This data is then given in the form of a bill💵 which can be used for various purposes of checking✔️ it.

Similarly, decorators can be used to create a logging functionality for functions to keep a record⏺️ of when the function is called and what values are passed to it.

This would help in tracking, finding, and debugging any issues.

Authentication and authorization

This is the functionality used everywhere these days. Login with 🌐 Google, Facebook, phone no. these are all ways of authenticating that the information is provided to the original user and not anyone random.

With decorators, you can add these checks✔️ to any function you need to like for logging in or signing up on your website.

The dashboard is only displayed to those who have an account.

caching

Caching is another popular functionality used all over the web🌐 and in apps📱. Whenever we visit a website for the second or third time it loads faster as our browser stores🍪 some data beforehand.

Decorators enable caching by storing the results of some expensive calculations🧮 and providing them directly when the method is called with same inputs again.

Rate Limiting

AI’s🤖 these days have some usage limits. Like, you can ask just 20 questions or you just have 100💰 credits.

These limits can also be put on the use of a function. You need to set a counter inside the decorator function and outside the wrapper🎁 function.

These are just a few use cases of decorators they have a much wider use base.

Conclusion

To wrap it up, we’ve covered:

  • the fundamentals of decorators,
  • including implementing them as functions,
  • using them on functions, and
  • exploring real-world🗺️ use cases.

We made it all clear💡 with our wedding💒 planning analogy and other examples.

In our next post, we’ll dive🏊‍♀️ into decorators as classes and decorators on classes.

If you ever come across some errors functool🛠️ library’s wraps decorator will help.

PEP official guide on decorators.

Challenge 🧗‍♀️

Your challenge is to create a Wedding💒 Planner Notification🔔 sender using Decorator. (refer to this)

Create a Python🐍 program that utilizes a decorator to send notifications🔔 for different stages of the wedding💒 planning process.

Stages:

@factory("Initial Planning")
def initial_planning():
    print("Determine the wedding date and time.")
    print("Set a budget.")
    print("Create a preliminary guest list.")
    print("Decide on the overall wedding style and theme.")
@factory("Selecting Venue And Vendor")
def venue_and_vendor():
    print("\n")
    print("Select venue and vendor based on planning.")
@factory("Decorate the Venue")
def decorate():
    print("\n")
    print("Decorate the venue based on theme.")
@factory("Send Invitations to guests.")
def invite():
    print("\n")
    print("Invite guests.")
@factory("Reharse a day before to ensure no mistakes on the day of wedding.")
def wedding_reharsal():
    print("\n")
    print("Reharse a day before the wedding.")
initial_planning()
venue_and_vendor()
decorate()
invite()
wedding_reharsal()

Leave a Reply