Make my_average(a, b) work with any a and b for which f_add and d_div are defined. As well as builtins

StackOverflow https://stackoverflow.com/questions/23550870

  •  18-07-2023
  •  | 
  •  

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?

Était-ce utile?

La solution

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

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'

Autres conseils

This might be an idea:

import operator

f_add = {}

def add(a,b):
    return f_add.get(type(a),operator.add)(a,b)


# example
class RGB:
    def __init__(self, r,g,b):
        self.r, self.g, self.b = (r,g,b)

    def __str__(self):
        return '<%s,%s,%s>'%(self.r,self.g,self.b)

f_add[RGB] = lambda a,b: RGB(a.r+b.r,a.g+b.g,a.b+b.b)
print(add(RGB(0.4,0.7,0.1), RGB(0.1, 0.2, 0.5)))
print(add(4,5))
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top