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 polymorphism is a way to make a language more expressive, while still maintaining full static type-safety. Using parametric polymorphism, a function or a data type can be written generically so that it can handle values identically without depending on their type. Such functions and data types are called generic functions and generic datatypes respectively and form the basis of generic programming.
For example, a function
append
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 type variable a denote the type of elements in the lists. Thenappend
can be typed
forall a. [a] × [a] -> [a]
Following Christopher Strachey, parametric polymorphism may be contrasted with ad hoc polymorphism, in which a 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 singledispatchmethod
.been added.
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 of a 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
temp: int@dataclass
class Coffee:
kind: str
temp: int@singledispatch
def boil(obj=None):
raise NotImplementedError("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 dispatch()
attribute:
>>> 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 registry
attribute:
>>> 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!