Make my_average(a, b) work with any a and b for which f_add and d_div are defined. As well as builtins
Question: 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` for which an
`f_add` and `f_div` have been defined. Without overloading + and / and without
breaking `my_average(built_in_type, built_in_type)` In python 3.

Specifically, I'm working with instances of a Pigment object I have created.
Overloading operators for these objects in not straightforward. For instance:

The difference (for distance purposes) between two instances might be `a.lab -
b.lab`. (The Lab colorspace has a good correlation between perceptive and
Euclidian distance.)

The sum (for mixing purposes) of two instances might be `a.srgb + b.srgb`.
(The srgb colorspace is linear and appropriate for mathematical manipulation.)

For other purposes, sum and difference might mean something else.

So, duck typing in my existing modules wont work.

    
    
    pigment_instance.distance(self, b)
    pigment_instance.mix(self, b)
    

are fine as long as I don't mind re-writing every function (as a method) I
need every time I have a new object like this. What I'd like to do is rewrite
my functions just once more to be more robust.

I've tried a few things:

    
    
    class Averager():
        __init__(self, f_mix, f_distance):
            self.f_mix = f_mix
            ...
        def __call__(self, a, b):
            # return the average calculated with self.f_something functions
    

That works OK, but I just end up burying an entire module in a class.

    
    
    def mix(a, b, f_mix=lambda x, y: x + y, f_distance=lambda x, y: x - y)
    # or, same as above with decorators.
    

Again, works OK, but I've got to keep the long default arguments or supply an
f_add every time I want to calculate 2+2.

    
    
    def pigment_functions(f_mix, f_distance):
        return [
            functools.partial(mix, f_mix=somefunc, f_distance=somefunc),
            functools.partial(distance, f_mix=somefunc, f_distance=somefunc)]
    
    mix, difference = pigment_functions(f_mix, f_distance)
    

A similar choice to the second.

    
    
    def mix(a, b):
        try: a + b
        except TypeError: # look for some global f_mix
    

Also works OK, but I've got global variables and a mess inside every function

Which of these (or something else) makes sense?


Answer: 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`](https://docs.python.org/dev/library/functools.html#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
    

### Example

    
    
    >>> 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'
    

