The world’s leading publication for data science, AI, and ML professionals.

Three Python Built-In Function Tricks Reducing Our Workloads

A hidden gem – Python functools + decorator

Image by John Leong from Pixabay
Image by John Leong from Pixabay

In one of my previous blogs, I have introduced one decorator in Python functools which can cache the intermediate results for recursive functions and web requests automatically. All we need to do is just adding a decorator to the function definition.

You Should Never Repeat Computing In Python

In this article, I’ll introduce three more useful features in the functools module. All of them can save us time from typing lots of code, and more importantly, make our code neat, tidy and more readable. Again, these are all built-in to Python 3.7 or above, so we don’t need to download anything.

Please be advised that all the code examples in this article are based on the functools module are imported as follows.

from functools import *

Importing everything might not be necessary in your case. You may want to import the specific object(s) only.

1. Total Ordering

Image by Janine Bolon from Pixabay
Image by Janine Bolon from Pixabay

The first one I would like to introduce is "Total Ordering". It is a decorator to be used for a class definition.

When we define a class and we want the instances from the class can be compared between each other such as greater than, equal and less than. Then, we will have to implement the corresponding methods such as __gt__(), __lt__() and __eq__(). If we want this class can handle all the comparing cases, we will have to implement all of these methods:

  • equal: __eq__()
  • greater than: __gt__()
  • less than: __lt__()
  • greater than or equal to: __ge__()
  • less than or equal to: __le__()

Let’s have a look at the example below. Suppose we are defining a class for Employees. We would like to compare employees based on their age.

class Employee:
    def __init__(self, name, age):
        self.name = name
        self.age = age
def __lt__(self, other):
        return self.age < other.age
def __eq__(self, other):
        return self.age == other.age

Please be noticed that I only defined the methods for "less than" and "equal".

Now, let’s have to instances from this Employee class.

e1 = Employee('Alice', 30)
e2 = Employee('Bob', 28)

Now, if we compare them using less than or equal, there won’t be any problems.

print(e1 < e2)
print(e1 == e2)

Since we never implemented __le__() for "less than or equal to", the comparison below will fail.

print(e1 <= e2)

The function tool "total ordering" can help in such scenarios. All we need to do is to add a decorator to the class as follows.

@total_ordering
class Employee:
    def __init__(self, name, age):
        self.name = name
        self.age = age
def __lt__(self, other):
        return self.age < other.age
def __eq__(self, other):
        return self.age == other.age

Now, we can compare two instances from the Employee class in whatever way we want.

print(e1 < e2)
print(e1 > e2)
print(e1 == e2)
print(e1 <= e2)
print(e1 >= e2)

With total ordering, we only need to implement the equal __eq__() and either one from __gt__(), __lt__(), __ge__() or __le__(). The total ordering function tool will help us to derive all the other comparison results.

Please also be noticed that this decorator might not be used in a performance-sensitive application because it does evaluate multiple comparison functions in order to "derive" the final results. For example, we have defined "less than" and "equal". Behind the scene, total ordering will evaluate both of them if we are comparing two objects with "less than or equal" conditions.

2. Partial Function

Image by Julio César Velásquez Mejía from Pixabay
Image by Julio César Velásquez Mejía from Pixabay

Have you ever defined several functions with very similar signatures and doing a very similar thing? Sometimes, we may need to consider using the partial function in Python.

Let me make up an example. Suppose we want to find all the words that start with "A" from a sentence. We may right a function like this.

def find_A_words(txt):
    return re.findall(r'Aw+', txt)

Please be advised that defining such a function might not be necessary but I just want to have a simple one because the regex is not the focus.

Now, we want to define another function to detect words that start with "B". So, we can define another one like this.

def find_B_words(txt):
    return re.findall(r'Bw+', txt)

We can test them and they should work.

print(find_A_words('Apple Ate Big Bad Air Bag'))
print(find_B_words('Apple Ate Big Bad Air Bag'))

With the partial function in Python functools module, we define the find_A_words() function and the find_A_words() function as "partial" functions of the re.findall() function.

find_A_words = partial(re.findall, r'Aw+')
find_B_words = partial(re.findall, r'Bw+')

As shown, the first argument should be the original function. Then, we can give partial argument(s) to the original function. Therefore, when we use the re-defined partial functions, we just need to give the rest of the arguments.

Specifically, for this example, the re.findall() function takes at least two arguments pattern and string. We have already passed the pattern using the partial function. Therefore, we only need to give the string argument when we use it.

print(find_A_words('Apple Ate Big Bad Air Bag'))
print(find_B_words('Apple Ate Big Bad Air Bag'))

The results are identical to the previous implementation.

3. Single Dispatch

Image by siala from Pixabay
Image by siala from Pixabay

Sometimes, we need to define a function that takes a parameter in which the data type is not deterministic. For example, it could be a string, integer or something else. Usually, we will have to write several if-else cases to handle this.

Single Dispatch in the Python functools module provides another way to solve this issue. We can define this logic for different data types in multiple functions. Sometimes, it will improve the readability of our code.

Suppose we are sending a greeting message to the user. It can be as easy as follows.

def greeting(name):
    print(f'Hi, {name}!')

If we pass the name as the argument, no problem.

greeting('Chris')

Now, what if we have a list of users and we want to greet them all? With the single dispatch function tool, we can first define the function using the decorator.

@singledispatch
def greeting(name):
    print(f'Hi, {name}!')

Then, we can register more overloaded versions to this function by using the register of the single dispatch.

@greeting.register
def _(name: list):
    name_string = ', '.join(name)
    print(f'Hi, {name_string}!')

Please be noticed that the function name will be not important at all, so I used the underscore for it.

Then, the single dispatch function tool will detect the signature and decide which function body should be used.

greeting(['Alice', 'Bob', 'Chris'])

Summary

Image by fancycrave1 from Pixabay
Image by fancycrave1 from Pixabay

In this article, I have introduced three function tools in the Python functools module. These small tricks may either reduce our efforts of coding or improve its readability. Use them when necessary!

Join Medium with my referral link – Christopher Tao

If you feel my articles are helpful, please consider joining Medium Membership to support me and thousands of other writers! (Click the link above)


Related Articles