Share @ Google LinkedIn Facebook  decorators, functools

functools - annotations

Please refer to first part of module to get idea about functions of module functools. This part will cover annotations.
In [1]:
import functools
  • @functools.lru_cache(maxsize=128,typed=False) - It's a decoractor which wraps a function to provide memoizing functionality. It saves upto maxsize function calls. It uses dictionary behind the scene to cache calls.

If maxsizeis None, then Least Recent Used Cache(LRU Cache) is functionality is disabled and cache can grow without limit.Cache performs best when LRU is of size power of 2.LRU cache performs best when most recent calls are best estimates of upcoming calls.

If typed is set to True then function argument of different types will be stored seprately.Ex: f(3) & f(3.0) will be stored differently.

Decorator adds cache_info() method to wrapped function which provides named_tuple of hits,misses, maxsize and currsize. It also provides cache_clear() function for clearing/invalidating cache.

In [2]:
@functools.lru_cache(maxsize=64)
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

def factorial_without_cache(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)
In [3]:
print(factorial_without_cache(4))
print(factorial(4))
24
24
In [4]:
%time print(factorial(6))
%time print(factorial_without_cache(6))
%time print(factorial(10))
%time print(factorial_without_cache(10))
720
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 223 µs
720
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 114 µs
3628800
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 55.3 µs
3628800
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 49.6 µs
In [5]:
factorial.cache_info()
Out[5]:
CacheInfo(hits=5, misses=10, maxsize=64, currsize=10)
In [6]:
factorial.cache_clear()
factorial.cache_info()
Out[6]:
CacheInfo(hits=0, misses=0, maxsize=64, currsize=0)
  • @functools.total_ordering : It's a class decorator. If class has defined one or more comparison methods then it adds rest of them. Class must define one of (__lt__(), __gt__(), __le__(),__ge__()) and __eq__() compulsarily.
In [7]:
@functools.total_ordering
class Employee(object):
    def __init__(self,name,age):
        self.name = name
        self.age =age

    def __eq__(self, other):
        return (self.name == other.name) and (self.age == other.age)

    def __lt__(self, other):
        return self.age < other.age
In [8]:
print(dir(Employee)) ## Notice __le__(), __ge__() and __gt__().
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
  • @functools.singledispatch: This decorator will convert normal function into single dispatch generic function. By single dispatch it means that based on type of first argument of function, it'l determine which generic function to execute.Generic function refers to function which performs same operation on different types of argument based on type of argument.

To overload function for particular data type register attribute of function is used. We can pass it datatype as attribute for data type it needs to be invoked.

In [9]:
@functools.singledispatch
def add(x, y):
    print('Inside Main Function')
    return x + y

@add.register(dict)
def dict_add(x, y):
    print('Inside dict version of  Function')
    x.update(y)
    return x

@add.register(set)
def set_add(x, y):
    print('Inside set version of  Function')
    return x.union(y)

@add.register(str)
def str_add(x, y):
    print('Inside string version of  Function')
    return x + y

@add.register(complex)
def complex_add(x, y):
    print('Inside complex version of  Function')
    return complex(x.real + y.real, x.imag + y.imag)
In [10]:
add([1,2,3],[4,5,6])
Inside Main Function
Out[10]:
[1, 2, 3, 4, 5, 6]
In [11]:
add('Coderz','Column')
Inside string version of  Function
Out[11]:
'CoderzColumn'
In [12]:
add({'a':'b'},{'c':'d'})
Inside dict version of  Function
Out[12]:
{'a': 'b', 'c': 'd'}
In [13]:
add(set([1,2,3]), set([3,4,5]))
Inside set version of  Function
Out[13]:
{1, 2, 3, 4, 5}
In [14]:
add(5+6j, 6+5j)
Inside complex version of  Function
Out[14]:
(11+11j)

When we don't find particular version for data type then it's method resolution order is used to find appropriate function. Original function decorated with @singledispatch is registered for object type which will be executed if no matching function found for particular data type.

One can find which function will be called for which data type based on dispatch(datatype) method of function.

In [15]:
print(add.dispatch(dict), add.dispatch(set), add.dispatch(complex), add.dispatch(bool))
<function dict_add at 0x7fb7db2db730> <function set_add at 0x7fb7db2db7b8> <function complex_add at 0x7fb7db2db8c8> <function add at 0x7fb7db2db158>
In [16]:
add.registry.keys()
Out[16]:
dict_keys([<class 'object'>, <class 'dict'>, <class 'set'>, <class 'str'>, <class 'complex'>])
In [17]:
print(add.registry[object], add.registry[dict], add.registry[set])
<function add at 0x7fb7db2db158> <function dict_add at 0x7fb7db2db730> <function set_add at 0x7fb7db2db7b8>
  • functools.wraps(wrapped,assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) - It's convenience decorator for functools.update_wrapper()(Functools - functions).
In [18]:
def outer2(func):
    def wrapper(*args):
        print('Before execution')
        func(*args)
        print('After execution')
    return wrapper

def print_title_2(title):
    """
    Simple print functionality
    """
    print(title)

print_title_wrapped = outer2(print_title_2)
print_title_wrapped('Welcome to CoderzColumn')
print('Attributes not updated even after applying decorator : '+ str(dict(zip(['__module__', '__name__', '__qualname__', '__doc__', '__annotations__','__dict__'],
                                                  [print_title_wrapped.__module__,print_title_wrapped.__name__, print_title_wrapped.__qualname__, print_title_wrapped.__doc__, print_title_wrapped.__annotations__, print_title_wrapped.__dict__]))))

def outer(func):
    @functools.wraps(func)
    def wrapper(*args):
        print('Before execution')
        func(*args)
        print('After execution')
    return wrapper

@outer
def print_title(title):
    """
    Simple print functionality
    """
    print(title)

print()
print_title('Welcome to CoderzColumn')
 ## Please make a not that document string also got updated otherwise it would be None withuot decorator and function name also got updated from wrapper to print_function.
print('Attributes updated due to decorator : '+ str(dict(zip(['__module__', '__name__', '__qualname__', '__doc__', '__annotations__','__dict__'],
                                                  [print_title.__module__,print_title.__name__, print_title.__qualname__, print_title.__doc__, print_title.__annotations__, print_title.__dict__]))))
Before execution
Welcome to CoderzColumn
After execution
Attributes not updated even after applying decorator : {'__module__': '__main__', '__name__': 'wrapper', '__qualname__': 'outer2.<locals>.wrapper', '__doc__': None, '__annotations__': {}, '__dict__': {}}

Before execution
Welcome to CoderzColumn
After execution
Attributes updated due to decorator : {'__module__': '__main__', '__name__': 'print_title', '__qualname__': 'print_title', '__doc__': '\n    Simple print functionality\n    ', '__annotations__': {}, '__dict__': {'__wrapped__': <function print_title at 0x7fb7db27c488>}}

Let other learners know about this article @ Google LinkedIn Facebook
Sunny Solanki  Sunny Solanki