Sometimes, you may need to implement a slight different variation of one function call depends on the type of the passed in parameter. Well you could have a long and tedious `switch`

or `if..else...`

statement together with `isinstance`

or `type`

. But there is a helper function to solve your parametric polymorphism problem.

In programming languages and type theory,

parametric polymorphismis a way to make a language moreexpressive, while still maintaining full static type-safety. Using parametric polymorphism, a function or a data type can be written generically so that it canhandle valuesSuch functions and data types are calledidenticallywithout depending on their type.generic functionsandgeneric datatypesrespectively and form the basis of generic programming.For example, a function

`that joins two lists can be constructed so that it does not care about the type of elements: it can append lists of integers, lists of real numbers, lists of strings, and so on. Let the`

appendtype variable adenote the type of elements in the lists. Then`can be typed`

append

forall a. [a] × [a] -> [a]Following Christopher Strachey, parametric polymorphism may be contrasted with

ad hoc polymorphism, in whicha single polymorphic function can have a number of distinct and potentially heterogeneous implementations depending on the type of argument(s) to which it is applied.Thus, ad hoc polymorphism can generally only support a limited number of such distinct types, since a separate implementation has to be provided for each type.— wiki

Starting Python 3.4. , there is a decorator in `functools`

called **singledispatch**** **been added. In Python 3.8, there is another decorator called

.been added.**singledispatchmethod**

Essentially, what these decorators do is to help **overload a function which is essentially a function with different implementations.** Calling an overloaded function will invoke one **implementation of** the many, which is a **generic function**, based on some **prior** **conditions.**

A generic function is composed of multiple functions implementing the same operation for different types. Which implementation should be used during a call is determined by the

dispatch algorithm. When the implementation is chosen based on the type ofa single argument,this is known as single dispatch.

Still too abstract? Let’s look at an example.

from functools import singledispatch

from decimal import Decimal"""

When there is no registered implementation found, its MRO is used to find a more generic implementation. Hence the original function decorated is registered for the base object type, and is used if no other implementation is found.

"""@singledispatch

def calc_num(num):

raise NotImplementedError("cannot calculate for unknown number type")@calc_num.register(int)

def calc_int(num):

print(f"int: {num}")@calc_num.register(float)

def calc_float(num):

print(f"float: {num}")"""

The decorator also supports decorator stacking, so we can create an overloaded function to handle multiple types.

"""@calc_num.register(float)

@calc_num.register(Decimal)

def calc_float_or_decimal(num):

print(f"float/decimal: {round(num, 2)}")calc_num(1)

calc_num(1.0)

calc_num(1.02324)

calc_num("num")===================int: 1

float/decimal: 1.0

float/decimal: 1.02

NotImplementedError: cannot calculate for unknown number type

You can also overload it with the customised function.

from functools import singledispatch

from dataclasses import dataclass@dataclass

class Tea:

kind:str@dataclass

temp: int

class Coffee:

kind:strtemp: int@singledispatch

def boil(obj=None):

raiseNotImplementedError("No boiler instruction for this drink")@process.register(Coffee)

def _coffee_boil(obj):return "Successfully boiled coffee!"@process.register(Tea)

def _tea_boil(obj):return "Successfully boiled tea!"

tea = Tea(kind="white tea", temp=93)

coffee = Coffee(kind="Yunnan", temp=98)boil(tea)

boil(coffee)Successfully boiled tea!

Successfully boiled coffee!

Since the `singledispatch`

can only dispatch based on the first argument passed, this becomes a problem with class method, as the default first `self`

argument will take the spot. In this case, we can use the* *`functools.singledispatchmethod, which t`

ransform a method into a single-dispatch generic function.

To define a generic method, decorate it with the `@singledispatchmethod`

decorator. Note that the dispatch happens on the type of the first non-self or non-cls argument, create your function accordingly:

class Negator:

@singledispatchmethod

def neg(self, arg):

raise NotImplementedError("Cannot negate a")@neg.register

def _(self, arg: int):

return -arg@neg.register

def _(self, arg: bool):

return not arg

To check which implementation will the generic function choose for a given type, use the

attribute:**dispatch**()

**>>> **fun.dispatch(float)

<function fun_num at 0x1035a2840>

**>>> **fun.dispatch(dict) *# note: default implementation*

<function fun at 0x103fe0000>

To access all registered implementations, use the read-only

attribute:**registry**

**>>> **fun.registry.keys()

dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,

<class 'decimal.Decimal'>, <class 'list'>,

<class 'float'>])

**>>> **fun.registry[float]

<function fun_num at 0x1035a2840>

**>>> **fun.registry[object]

<function fun at 0x103fe0000>

That’s so much of it!

Happy Reading!