functools

Page content
  • It is for higher-order functions that work on other functions.
  • It provides functions for working with other functions and callable objects to use or extend them without completely rewriting them.

Classes

partial

  • It takes a function that accepts some arguments and turns it into a function that accepts fewer arguments.
"""
partial(func[, *args][, **keywords])

- It is an original function for particular argument values.
- Objects created by `partial()` have three read-only attributes:
  - `partial.func` returns the name of the parent function along with a hexadecimal address.
  - `partial.args` returns the positional arguments provided in the partial function.
  - `partial.keywords` returns the keyword arguments provided in the partial function.
"""

from functools import partial


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

# partial functions
pow2 = partial(power, b = 2)
pow4 = partial(power, b = 4)
power_of_5 = partial(power, 5)

print(power(2, 3)) # 8
print(pow2(4)) # 16
print(pow4(3)) # 81
print(power_of_5(2)) # 25

print('Function used in partial function pow2 :', pow2.func) # Function used in partial function pow2 : <function power at 0x000001CCBBE8C7B8>
print('Default keywords for pow2 :', pow2.keywords) # Default keywords for pow2 : {‘b’: 2}
print('Default arguments for power_of_5 :', power_of_5.args) # Default arguments for power_of_5 : (5, )

###

def add_numbers(x, y):
  return x + y

# adds 5 to its argument
add_five = partial(add_numbers, 5)

###

partialmethod

"""partialmethod(func, *args, **keywords)
- It is a method definition of an already defined function for specific arguments like partial function.
- It is not callable, only a method descriptor that returns a new partialmethod descriptor.
"""

from functools import partialmethod 
  
class Demo: 
    def __init__(self): 
        self.color = 'black'
    def _color(self, type): 
        self.color = type
  
    set_red = partialmethod(_color, type ='red') 
    set_blue = partialmethod(_color, type ='blue') 
    set_green = partialmethod(_color, type ='green') 
  
  
obj = Demo() 
print(obj.color) # black 
obj.set_blue()
print(obj.color) # blue

Functions

Wrap

  • It is a built-in function under the functools module.
  • It delivers extra information from the function f(argument) to the wrapper function.
"""It applies update_wrapper() to the decorated function.
"""

from functools import wraps

def decorator(f):
 @wraps(f)
 def decorated(*args, **kwargs):
  """Decorator's docstring"""
  return f(*args, **kwargs)

 print('Documentation of decorated : ', decorated.__doc__)
 return decorated

@decorator
def f(x):
 """f's Docstring"""
 return x

print('f name :', f.__name__) 
print('Documentation of f :', f.__doc__)

# Documentation of decorated : f's Docstring
# f name : f
# Documentation of f : f's Docstring

###

""" To avoid the confusion that might be caused by the wrapper
function, use @functools.wraps decorator in the wrapper function, 
which will preserve information about the original function.

@functools.wrap(<function_name>)
def wrapper(*args, **kwargs):
  <function_name>() ...
"""

from functools import wraps

def my_logger(original_function):
  
  import logging

  logging.basicConfig(filename=f'{original_function.__name__}.log', level=logging.INFO)

  @wraps(original_function)
  def wrapper(*args, **kwargs):
    logging.info(f'Ran with args: {args}, and kwargs: {kwargs}')
    return original_function(*args, **kwargs)

  return wrapper

def my_timer(original_function):

  import time
  
  @wraps(original_function)
  def wrapper(*args, **kwargs):
    start_time = time.time()
    result = original_function(*args, **kwargs)
    run_time = time.time() - start_time
    print(f'{original_function.__name__} ran in: {run_time:.4f} sec')
    return result

  return wrapper

@my_timer
@my_logger
def display_info(name, age):

  import time

  time.sleep(1)
  print(f'display_info ran with arguments ({name}, {age})')

display_info("Hank", 30)

# https://www.youtube.com/watch?v=FsAPt_9Bf3U&ab_channel=CoreySchafer

###

update_wrapper

"""update_wrapper(<wrapper_function_name>, <wrapped_function_name>[, assigned][, updated])
It updates a wrapper function to look like the wrapped function.
"""

from functools import partial, update_wrapper

def power(a, b):
 """ a to the power b """
 return a**b

# partial function
pow2 = partial(power, b = 2)
pow2.__doc__ = """a to the power 2"""
pow2.__name__ = "pow2"

print(f'Before: {pow2.__doc__}, {pow2.__name__}') # a to the power 2, pow2

update_wrapper(pow2, power) 

print(f'After: {pow2.__doc__}, {pow2.__name__}')  # a to the power b, power

total_ordering

"""It provides rich class comparison methods that help in comparing
classes without explicitly defining a function for it.
at least one of the comparison methods must be defined from
__lt__, __le__, __eq__, __neq__, __gt__, __ge__
i.e., object.__lt__(self, other) eq function must also be defined.
"""

from functools import total_ordering

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

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

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

bob = Student(8.6)
alice = Student(7.5)

print(bob.__eq__(alice)) # False

singledispatch

"""It transforms a function into a generic function.
It is used for function overloadings. The overloaded
implementations are registered using the register()
attribute.
"""

from functools import singledispatch 

@singledispatch
def fun(s): 
    print(s) 

@fun.register(int) 
def _(s): 
    print(s * 2) 

fun('GeeksforGeeks') # GeeksforGeeks
fun(10) # 20

reduce

"""
reduce(<function>, sequence[, initial]) -> value

It applies a function of two arguments repeatedly on the elements
of a sequence to reduce the sequence to a single value.

i.e. reduce(lambda x, y: x^y, [1, 2, 3, 4])) calculates (((1^2)^3)^4)
"""

from functools import reduce

list1 = [2, 4, 7, 9, 1, 3]
sum_of_list1 = reduce(lambda a, b:a + b, list1) # ((2+4)+7+9+1+3)
print("Sum of list1:", sum_of_list1) # 26

list2 = ["abc", "xyz", "def"]
max_of_list2 = reduce(lambda a, b:a if a>b else b, list2) # ((abc, xyz), def)
print("Max of list2:", max_of_list2) # xyz

lru_cache

"""It is used for saving up to the maxsize most recent calls of
a function. It speeds up consecutive runs of functions and operations
using cache.
"""

from functools import lru_cache 

@lru_cache(maxsize = None) 
def factorial(n):
    return n * factorial(n-1) if n >= 1 else 1

print([factorial(n) for n in range(7)]) 
print(factorial.cache_info())

# [1, 1, 2, 6, 24, 120, 720]
# CacheInfo(hits=5, misses=7, maxsize=None, currsize=7)

"""Recursive functions can benefit from lru_cache. Whenever
we run this function, the first few factorial calculations will be
saved into the cache. As a result, next time we go to call the function
we will only need to calculate the factorials that are after the ones
we used prior. Of course, not all factorial calculations will be saved.
"""

cmp_to_key

""" <function_name>(<iterable_name>, cmp_to_key(<comparison_function>)
It converts a comparison function into a key function.
It must return 1, 0, -1 for different conditions.
"""

from functools import cmp_to_key

def cmp_fun(a, b):
 if a[-1] > b[-1]:
  return 1
 elif a[-1] < b[-1]:
  return -1
 else:
  return 0

list1 = ['Bob', 'Alice', 'Carol']
l = sorted(list1, cmp_to_key(cmp_fun))
print('sorted list:', l) # ['Alice', 'Bob', 'Carol']