In short: what I want is for the majority of mathematical functions I\'ve written (e.g., my_average(a, b)
) to work with any a
and b
fo
if you have my_average(a, b)
that is implemented in terms of add
and div
functions e.g.:
def my_average(a, b):
return div(add(a, b), 2)
then to provide different implementations for different types, you could use functools.singledispatch:
import functools
@singledispatch
def div(x, y:int): # default implementation
raise NotImplementedError('for type: {}'.format(type(x)))
@div.register(Divisible) # anything with __truediv__ method
def _(x, y):
return x / y
@singledispatch
def add(a, b):
raise NotImplementedError('for type: {}'.format(type(a)))
@add.register(Addable) # anything with __add__ method
def _(a, b):
return a + b
where Addable
, Divisable
could be defined as:
from abc import ABCMeta, abstractmethod
class Divisible(metaclass=ABCMeta):
"""Anything with __truediv__ method."""
__slots__ = ()
__hash__ = None # disable default hashing
@abstractmethod
def __truediv__(self, other):
"""Return self / other."""
@classmethod
def __subclasshook__(cls, C):
if cls is Divisible:
if any("__truediv__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
class Addable(metaclass=ABCMeta):
"""Anything with __add__ method."""
__slots__ = ()
__hash__ = None # disable default hashing
@abstractmethod
def __add__(self, other):
"""Return self + other."""
@classmethod
def __subclasshook__(cls, C):
if cls is Addable:
if any("__add__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
>>> isinstance(1, Addable) # has __add__ method
True
>>> isinstance(1, Divisible) # has __truediv__ method
True
>>> my_average(1, 2)
1.5
>>> class A:
... def __radd__(self, other):
... return D(other + 1)
...
>>> isinstance(A(), Addable)
False
>>> _ = Addable.register(A) # register explicitly
>>> isinstance(A(), Addable)
True
>>> class D:
... def __init__(self, number):
... self.number = number
... def __truediv__(self, other):
... return self.number / other
...
>>> isinstance(D(1), Divisible) # via issubclass hook
True
>>> my_average(1, A())
1.0
>>> my_average(A(), 1) # no A.__div__
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'A' and 'int'
Builtin numbers such as int
define __add__
, __truediv__
method so they are supported automatically. As class A
shows, you could use classes even if they don't define the specific methods such as __add__
by calling .register
method explicitly if they still can be used in the given implementation.
Use add.register
and div.register
to define implementations for other types if necessary e.g.:
@div.register(str)
def _(x, y):
return x % y
After that:
>>> my_average("%s", "b") # -> `("%s" + "b") % 2`
'2b'