Decorators in Python – Extend Your Code

decorating a function in Python

Python decorators provide a readable way to extend the behavior of a function, method, or class.

Decorating a function in Python follows this syntax:

@guard_zero
def divide(x, y):
    return x / y

Here the guard_zero decorator updates the behavior of divide() function to make sure y is not 0 when dividing.

How to Use Decorators in Python

The best way to demonstrate using decorators is by an example.

Let’s first create a function that divides two numbers:

def divide(x, y):
    return x / y

The issue with this function is it allows divisions by 0, which is illegal mathematically. You could solve this problem by adding an if check.

However, there is another option called decorators. Using a decorator, you do not change the implementation of the function. Instead, you extend it from outside. For now, the benefit of doing this is not apparent. We will come back to it later on.

Let’s start by creating the guard_zero decorator function that:

  1. Takes a function as an argument.
  2. Creates an extended version of it.
  3. Returns the extended function.

Here is how it looks in code:

def guard_zero(operate):
    def inner(x, y):
        if y == 0:
            print("Cannot divide by 0.")
            return
        return operate(x, y)
    return inner

Here:

  • The operate argument is a function to extend.
  • The inner function is the extended version of the operate function. It checks if the second input argument is zero before it calls operate.
  • Finally, the inner function is returned. It is the extended version of operate, the original funtion passed as an argument.

You can now update the behavior of your divide function by passing it into the guard_zero. This happens by reassigning the extended divide function to the original one:

divide = guard_zero(divide)

Now you have successfully decorated the divide function.

However, when talking about decorators, there is a more Pythonic way to use them. Instead of passing the extended object as an argument to the decorator function you can “mark” the function with the decorator using the @ symbol:

@guard_zero
def divide(x, y):
    return x / y

This is a more convenient way to apply decorators in Python. It also looks syntactically nice and the intent is clear.

Now you can test that the divide function was really extended with different inputs:

print(divide(5, 0))

print(divide(5, 2))

Output:

Cannot divide by 0.
None

2.5

(A None appears in the output because guard_zero returns None when y is 0.)

Here is the full code used in this example for your convenience:

def guard_zero(operate):
    def inner(x, y):
        if y == 0:
            print("Cannot divide by 0.")
            return
        return operate(x, y)
    return inner

@guard_zero
def divide(x, y):
    return x / y
    
print(divide(5, 0)) # prints "Cannot divide by 0"

Now you know how to use a decorator to extend a function. But when is this actually useful?

When to Use Decorators in Python

Why all the hassle with a decorator? In the previous example, you could have created an if-check and saved 10 lines of code.

Yes, the decorator in the previous example was overkill. But the power of decorators becomes clear when you can avoid repetition and improve overall code quality.

Imagine you have a bunch of similar functions in your project:

def checkUsername(name):
    if type(name) is str:
        print("Correct format.")
    else:
        print("Incorrect format.")
    print("Handling username completed.")

def checkName(name):
    if type(name) is str:
        print("Correct format.")
    else:
        print("Incorrect format.")
    print("Handling name completed.")

def checkLastName(name):
    if type(name) is str:
        print("Correct format.")
    else:
        print("Incorrect format.")
    print("Handling last name completed.")

As you can see, these functions all have the same if-else statement for input validation. This introduces a lot of unnecessary repetition in the code.

Let’s improve this piece of code by implementing an input validator decorator. In this decorator, we perform the repetitive if-else checks altogether:

def string_guard(operate):
    def inner(name):
        if type(name) is str:
            print("Correct format.")
        else:
            print("Incorrect format.")
        operate(name)
    return inner

This decorator:

  • Takes a function as an argument.
  • Extends the behavior to check if the input is a string.
  • Returns the extended function.

Now, instead of repeating the same if-else in each function, you can decorate each function with the function that performs the if-else checks:

@string_guard
def checkUsername(name):
    print("Handling username completed.")

@string_guard
def checkName(name):
    print("Handling name completed.")

@string_guard
def checkLastName(name):
    print("Handling last name completed.")

This is much cleaner than the if-else mess. Now the code is more readable and concise. Better yet, if you need more similar functions in the future, you can apply the string_guard to those as well.

Now you know how decorators can help you write cleaner code and reduce unwanted repetition.

Next, let’s take a look at some common built-in decorators you need to know about in Python.

@Property Decorator in Python

Decorating a method in a class with @property makes it possible to call a method like accessing an attribute:

weight.pounds() ---> weight.pounds

Let’s see how it works and when you should use it.

Example

Let’s create a Mass class that stores mass in kilos and pounds:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
        self.pounds = kilos * 2.205

You can use this class as follows:

mass = Mass(1000)

print(mass.kilos)
print(mass.pounds)

Output:

1000
2205

Now, let’s modify the number of kilos, and see what happens to pounds:

mass.kilos = 1200
print(mass.pounds)

Output:

2205

Changing the number of kilos did not affect the number of pounds. This is because you did not update the pounds. Of course, this is not what you want. It would be better if the pounds property would be updated at the same time.

To fix this, you can replace the pounds attribute with a pounds() method. This method computes the pounds on-demand based on the number of kilos.

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
            
    def pounds(self):
        return self.kilos * 2.205

Now you can test it:

mass = Mass(100)
print(mass.pounds())

mass.kilos = 500
print(mass.pounds())

Result:

220.5
1102.5

This works like a charm.

However, now calling mass.pounds does not work as it is no longer a variable. Thus, if you call mass.pounds without parenthesis anywhere in the code, the program crashes. So even though the change fixed the problem, it introduced syntactical differences.

Now, you could go through the whole project and add the parenthesis for each mass.pounds call.

But there is an alternative.

Use the @property decorator to extend the pounds() method. This turns the method into a getter method. This means it is still accessible similar to a variable even though it is a method. In other words, you do not need to use parenthesis with this method call:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
        
    @property
    def pounds(self):
        return self.kilos * 2.205

For example:

mass = Mass(100)
print(mass.pounds)

mass.kilos = 500
print(mass.pounds)

Using the @property decorator thus reduces the risk of making the old code crash due to the changes in syntax.

@Classmethod Decorator in Python

A class method is useful when you need a method that involves the class but is not instance-specific.

A common use case for class methods is a “second initializer”.

To create a class method in Python, decorate a method inside a class with @classmethod.

Class Method as a Second Initializer in Python

Let’s say you have a Weight class:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos

You create Weight instances like this:

w = Weight(100)

But what if you wanted to create a weight from pounds instead of kilos? In this case, you need to convert the number of kilos to pounds beforehand:

pounds = 220.5
kilos = pounds / 2.205

w2 = Weight(kilos)

But this is bad practice and if done often, it introduces a lot of unnecessary repetition in the code.

What if you could create a Weight object directly from pounds with something like weight.from_pounds(220.5)?

To do this, you can write a second initializer for the Weight class. This is possible by using the @classmethod decorator:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos
    
    @classmethod
    def from_pounds(cls, pounds):
        kilos = pounds / 2.205
        return cls(kilos)

Let’s take a look at the code to understand how it works:

  • The @classmethod turns the from_pounds() method into a class method. In this case, it becomes the “second initializer”.
  • The first argument cls is a mandatory argument in a class method. It’s similar to self. The cls represents the whole class, not just an instance of it.
  • The second argument pounds is the number of pounds you are initializing the Weight object form,
  • Inside the from_pounds method, the pounds are converted to kilos.
  • Then the last line returns a new Weight object generated from pounds. (cls(kilos) is equivalent to Weight(kilos))

Now it is possible to create a Weight object directly from a number of pounds:

w = Weight.from_pounds(220.5)
print(w.kilos)

Output:

100

@Staticmethod Decorator in Python

A static method in Python is a method tied to a class, not to an instance of it

A static method could also be a separate function outside the class. But as it closely relates to the class, it is placed inside of it.

A static method does not take reference argument self because it cannot access or modify the attributes of a class. It’s an independent method that works the same way for each object of the class.

To create a static method in Python, decorate a method in a class with the @staticmethod decorator.

For example, let’s add a static method conversion_info into the Weight class:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos
    
    @classmethod
    def from_pounds(cls, pounds):
        kilos = pounds / 2.205
        return cls(kilos)
    
    @staticmethod
    def conversion_info():
        print("Kilos are converted to pounds by multiplying by 2.205.")

To call this method, you can call it on the Weight class directly instead of creating a Weight object to call it on.

Weight.conversion_info()

Output:

Kilos are converted to pounds by multiplying by 2.205.

Because the method is static, you can also it on a Weight object.

Conclusion

In Python, you can use decorators to extend the functionality of a function, method.

For example, you can implement a guard_zero decorator to prevent dividing by 0. Then you can extend a function with it:

@guard_zero
def divide(x, y):
    return x / y

Decorators are useful when you can avoid repetition and improve code quality.

There are useful built-in decorators in Python such as @property, @classmethod, and @staticmethod. These decorators help you make your classes more elegant. Under the hood, these decorators extend the methods by feeding them into a decorator function that updates the methods to do something useful.

Thanks for reading.

Happy coding!

Further Reading

50 Python Interview Questions and Answers

50+ Buzzwords of Web Development

Share this article with others:

Share on twitter
Share on linkedin
Share on facebook
Share on pinterest
Share on email

Learn these 10 tips to write professional code.

 

Make your coding life easier with these amazing command-line tools

 

Learn how to use latest advancements in AI to write faster.

 

Leave a Comment

Your email address will not be published.