6 Python decorators that will make your code much easier


“Simple is better than complex.”

The best Python feature that applies this “Zen of Python” philosophy is the decorator.

Decorators can help you write less code to implement complex logic and reuse it everywhere.

What’s more, there are many great Python built-in decorators that make our lives a lot easier because we can just use one line of code to add complex functionality to existing functions or classes.

I will not chat. Let’s take a look at 6 decorators I’ve selected that will show you just how elegant Python is.

1. @lru_cache: Speed ​​up programs with caching

The easiest way to speed up Python functions with caching tricks is to use the @lru_cache decorator.

This decorator can be used to cache the results of a function so that subsequent function calls with the same arguments will not be executed again.

This is especially useful for functions that are computationally expensive or are often called with the same arguments.

Consider an intuitive example:

import time


def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


start_time = time.perf_counter()
print(fibonacci(30))
end_time = time.perf_counter()
print(f"The execution time: {end_time - start_time:.8f} seconds")
# The execution time: 0.18129450 seconds

The above program calculates the Nth Fibonacci number using a Python function. This takes a lot of time because when calculating fibonacci(30), many previous Fibonacci numbers will be calculated many times in the process of recursion.

Now let’s speed up this process with the @lru_cache decorator:

from functools import lru_cache
import time


@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


start_time = time.perf_counter()
print(fibonacci(30))
end_time = time.perf_counter()
print(f"The execution time: {end_time - start_time:.8f} seconds")
# The execution time: 0.00002990 seconds

As you can see from the code above, after using the @lru_cache decorator, we can get the same result in 0.00002990 seconds, which is much faster than the previous 0.18129450 seconds.

The @lru_cache decorator has a maxsize parameter that specifies the maximum number of results to store in the cache. When the cache is full and a new result needs to be stored, the least used result is evicted from the cache to make room for the new one. This is called the least used result (LRU) strategy.

By default, maxsize is set to 128. If it is set to None, as in our example, the LRU features are disabled and the cache can grow without limit.

2. @total_ordering: Adding missing comparison methods

The @total_ordering decorator from the functools module is used to generate missing comparison methods for a Python class based on those defined.

Here is an example:

from functools import total_ordering


@total_ordering
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def __eq__(self, other):
        return self.grade == other.grade

    def __lt__(self, other):
        return self.grade < other.grade


student1 = Student("Alice", 85)
student2 = Student("Bob", 75)
student3 = Student("Charlie", 85)

print(student1 < student2)  # False
print(student1 > student2)  # True
print(student1 == student3)  # True
print(student1 <= student3) # True
print(student3 >= student2) # True

As you can see from the code above, there are no method definitions in the Student class ge, gt and le. However, thanks to the @total_ordering decorator, the results of our comparisons between different instances will be correct.

The advantages of this decorator are obvious:

  • It can make your code cleaner and save you time. Because you don’t have to write all the comparison methods.

  • Some older classes may not define enough comparison methods. It’s safer to add the @total_ordering decorator to it for later use.

3. @contextmanager: Custom context manager

Python has a mechanism context managementwhich will help you properly manage resources.

Basically we just need to use with statements:

with open("test.txt",'w') as f:
    f.write("Yang is writing!")

As shown in the code above, we can open a file using the with statement so that it is closed automatically after writing. We don’t need to explicitly call the f.close() function to close the file.

Sometimes we need to define a custom context manager for some specific requirement. In this case, the @contextmanager decorator is our friend.

For example, the following code implements a simple, custom context manager that can display relevant information when a file is opened or closed.

from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
    print("The file is opening...")
    file = open(filename,mode)
    yield file
    print("The file is closing...")
    file.close()

with file_manager('test.txt', 'w') as f:
    f.write('Yang is writing!')
# The file is opening...
# The file is closing...

4. @property: Set up getters and setters for classes

Getters and setters are important concepts in object-oriented programming (OOP).

For each class instance variable, the getter method returns its value, and the setter method sets or updates its value. Given this, getters and setters are also known as accessors and mutators, respectively.

They are used to protect data from direct and unexpected access or modification.

Different OOP languages ​​have different mechanisms for defining getters and setters. In Python, we can simply use the @property decorator.

class Student:
    def __init__(self):
        self._score = 0

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, s):
        if 0 <= s <= 100:
            self._score = s
        else:
            raise ValueError('The score must be between 0 ~ 100!')

Yang = Student()

Yang.score=99
print(Yang.score)
# 99

Yang.score = 999
# ValueError: The score must be between 0 ~ 100!

As you can see from the example above, the score variable cannot be set to 999, which is a nonsensical number. Because we’ve limited its allowed range inside the setter function with the @property decorator.

No doubt adding this setter can successfully avoid unexpected errors or results.

5. @cached_property: Cached the result of a function as an attribute

In Python 3.8, the functools module has a powerful new decorator called @cached_property. It can turn a class method into a property whose value is evaluated once and then cached as a normal attribute for the life of the instance.

Here is an example:

from functools import cached_property


class Circle:
    def __init__(self, radius):
        self.radius = radius

    @cached_property
    def area(self):
        return 3.14 * self.radius ** 2


circle = Circle(10)
print(circle.area)
# prints 314.0
print(circle.area)
# returns the cached result (314.0) directly

In the code above, we have optimized the area method through the @cached_property property. Thus, there is no recalculation for circle.area of ​​the same immutable instance.

6. @atexit.register: We declare a function that is called when the program exits

The @register decorator from the atexit module can allow us to execute a function when the Python interpreter exits.

This decorator is very useful for final tasks like freeing resources or just saying goodbye! 👋

Here is an example:

import atexit

@atexit.register
def goodbye():
    print("Bye bye!")

print("Hello Yang!")

At the output we get

Hello Yang!
Bye bye!

For more examples of using Python and Machine Learning in modern services, see my telegram channel. I write about development, ML, startups and relocation in the UK for IT professionals.

Similar Posts

Leave a Reply

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