The functools module in the standard library   provides functions and classes for manipulating functions. The primary tool defined in the module is the partial class, which as we will see in a while, allows the partial application of a function. This means that a callable object is created with some of the arguments already filled in, reducing the number of arguments that must be supplied to subsequent calls. 


The module also provides a number of decorators which can be used to wrap functions and classes in order to extend them with additional capabilities.

Partial Objects

The partial class makes it possible to create a version of a callable object( e.g functions) with some of the arguments pre-filled. 

partial(callable, *args, **kwargs)
#import the functools module
import functools

myprint = functools.partial(print, sep = ', ')

#use the partial function
myprint(*range(10))
myprint('Python', 'C++', 'Java', 'Ruby', 'Javascript')

In the above example, we created a new partial function, myprint(), from  the builtin print()  function. The original print() function would have separated each item with a space, but by using  functools.partial function, we were able to set the sep keyword argument to a comma and a space( ', ').   

In the following example we use a user-defined function to create a partial function.

import functools

def evaluate(num1, num2, oper):
    """Evaluates the arithmetic result of applying operator, 'oper' on 'num1' and 'num2'"""

    num1, num2 = int(num1), int(num2)

    result = None
    match oper:
        case "+":
            result = num1 + num2
        case "-":
            result = num1 - num2
        case "*":
            result = num1 * num2
        case "/":
            result = num1 / num2
        case "%":
            result = num1 % num2
        case "**":
            result = num1 ** num2
        case _:
            return ("Invalid operator '%s'"%oper)

    return f"{num1} {oper} {num2} = {result}"

add = functools.partial(evaluate, oper = '+')

print(add(3, 5))
print(add(10, 20))
print(add(50, 60))

The following example uses a class rather than a function as the callable

import functools

class Person:
   def __init__(self, name, country, nationality):
      self.name = name
      self.country = country
      self.nationality = nationality

   def info(self):
      return f"""name: {self.name}
country: {self.country}
nationality: {self.nationality}"""

#create a partial constructor for people of a specific country
indian = functools.partial(Person, country = 'India', nationality = "Indian")

p1 = indian('Rahul')
print(p1.info())

Acquire properties of the original callable

By default, the returned partial object, does not inherit the __name__ and the __doc__ attributes  from the original callable object.This attributes are very essential especially for debugging purposes.

import functools

myprint = functools.partial(print, sep = ', ')

#get the __doc__ attribute
print(myprint.__doc__)

As you can see above, myprint() function does not inherit the docstring of the  print() function, from which it is a partial object.

The update_wrapper() function attaches the relevant information to the partial object from another object.

import functools

myprint = functools.partial(print, sep = ', ')

functools.update_wrapper(myprint, print)


#get the __doc__ attribute
print(myprint.__doc__)

partialmethod class

The partial class, as we have seen, works with bare functions. The partialmethod class similarly creates partial objects but from methods.  The partial method should be defined inside the class as shown below. 

import functools

class Person:
   def __init__(self, name, country, nationality):
      self.name = name
      self.country = country
      self.nationality = nationality

   def change_nationality(self, new_country, new_nationality):
       self.country = new_country
       self.nationality = new_nationality

   #create a partial metthod for 'change_nationality'
   to_japanese = functools.partialmethod(change_nationality, new_country = 'Japan', new_nationality = 'Japanese')

p1 = Person('Rahul', 'India', 'Indian')
print(p1.name, p1.country, p1.nationality)

#call the partialmethod 'to_japanese' on object
p1.to_japanese()
print(p1.name, p1.country, p1.nationality)

Functions defined in the functools module

Apart from the two classes that we have looked at i.e partial and partialmethod, the module also defines several utility functions that can be used to further manipulate functions through decoration. 

cmp_to_key()

Some high order functions such builtin sorted(), filter(), max(), and min(), takes an optional parameter called key which is used to specify a particular function to be applied to each element of the iterable prior to making comparisons. 

L = [-10, -2, 5, 0, -1, 4]

#the key function
def absolute(x):
     if x < 0:
        return -x
     return x

#sort the elements by their absolute value
print(sorted(L, key = absolute))

The  cmp_to_key()  function is used to create a key function from a traditional comparison function. The function given as an argument  must return either 1, 0, or -1 depending on the arguments it receives.

import functools

L = [('Python', 3), ('Java', 4),  ('C++', 2), ('Javascript', 1), ('PHP', 5)]


# function to Sort the tuples by their second items

@functools.cmp_to_key
def func(a, b):
    if a[-1] > b[-1]:
        return 1
    elif a[-1] < b[-1]:
        return -1
    else:
        return 0

#sort the elements by their last element
print(sorted(L, key = func))

reduce()

The reduce() function applies a given function cumulatively to an iterable. It typically takes two arguments: a function and an iterable, it then applies the function cumulatively on the elements of the given iterable.

reduce(func, iterable)

The function given as func must accept two arguments. 

import functools

L = [1, 2, 3, 4, 5, 6, 7, 8, 9]

def add(a, b):
    return a + b

cumulative_sum = functools.reduce(add, L)

print(cumulative_sum)
import functools

L = [1, 2, 3, 4, 5, 6, 7, 8, 9]

def prod(a, b):
    return a * b

cumulative_prod = functools.reduce(prod, L)

print(cumulative_prod)

cache()

The cache() function is used  to cache the result of an expensive computation for future use. This means that if the same function is called with the same parameters, the results are cached and the computation does not need to be redone.

import functools 

@functools.cache
def fibonacci(num): 
    if num in [0, 1]: 
       return num 
    return fibonacci(num - 1) + fibonacci(num - 2)

# Let's run it and check the cache 
print(fibonacci(10))
print(fibonacci(10))

lru_cache()

The lru_cache() function implements a least recently used (LRU) cache for efficient memoization of a function.  When the function is called, the lru_cache() will store any inputs and outputs of the function in an order of least recently used. When the cache is full, the least recently used items are discarded to make space for new data. This is beneficial as it allows the cache to store more relevant data rather than having to store all data from the function call.

import functools 

@functools.lru_cache(maxsize=4) 
def fibonacci(num): 
    if num in [0, 1]: 
       return num 
    return fibonacci(num - 1) + fibonacci(num - 2)

# Let's run it and check the cache 
print(fibonacci(10))
print(fibonacci(10))

singledispatch()

The singledispatch() decorator function  is used to create functions that can dispatch on the type of a single argument such that certain behaviors are dependent on the type. When decorated with singledispatch(), a function becomes a "generic function", meaning that it can have multiple different implementations, depending on the type of the argument passed to it.

The implementation for a particular type is registered using the register() method of the decorated function.

import functools 

@functools.singledispatch 
def add(a, b):
    raise NotImplementedError

@add.register(str) 
def _(a, b): 
    return f"{a} {b}" 

@add.register(int) 
def _(a, b): 
    return a + b 

#with ints
print(add(1, 2)) 

#with strings
print(add("Hello", "World"))

#an error is raised for a non-implemented types
print(add([1, 2], [3, 4]))

@total_ordering

The @functools.total_ordering() is used to automatically fill in comparison methods for a class by defining only  two of the six rich comparison methods (__lt__, __le__, __eq__, __ne__, __gt__ and __ge__) . This is typically achieved by decorating the class with the @total_ordering decorator and defining the __eq__  method alongside any other of (__lt__, __le__,__gt__, __ge__).

import functools

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

    def __repr__(self): 
        return self.name 

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

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

john = Student("John", 83) 
jane = Student("Jane", 87) 

print(john == jane) 
print(john != jane)
print(john < jane) 
print(john <= jane)
print(john > jane)
print(john >= jane)