使my_average(a,b)与定义了f_add和d_div的任何a和b一起使用。以及内置


问题内容

简而言之:我想要的是我编写的大多数数学函数(例如my_average(a, b))都可以使用a和定义bf_add和的任何数学函数f_div。没有重载+和/且没有破坏my_average(built_in_type, built_in_type)python 3。

具体来说,我正在使用我创建的颜料对象的实例。这些对象的重载运算符并不简单。例如:

两个实例之间的差异(出于距离目的)可能是a.lab - b.lab。(Lab色彩空间在感知距离和欧几里得距离之间具有良好的相关性。)

两个实例的总和(出于混合目的)可能是a.srgb + b.srgb。(srgb颜色空间是线性的,适用于数学操作。)

对于其他目的,总和与差额可能意味着其他。

因此,在现有模块中键入鸭子将不起作用。

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

只要我不介意在每次有这样的新对象时都重写每个函数(作为一种方法)就可以了。我想做的就是再次重写我的函数,以使其更强大。

我已经尝试了几件事:

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

可以,但是最后我将整个模块都埋在了一个类中。

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

同样,可以正常工作,但是每次我要计算2 + 2时,我必须保留长的默认参数或提供一个f_add。

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)

与第二个相似的选择。

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

也可以,但是我在每个函数中都有全局变量和混乱

其中哪一个(或其他)有意义?


问题答案:

如果您已经my_average(a, b)按照adddiv功能实现了,例如:

def my_average(a, b):
    return div(add(a, b), 2)

然后为不同的类型提供不同的实现,可以使用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

其中AddableDivisable可以定义为:

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'

内置数字,例如intdefine
__add____truediv__method,因此它们自动得到支持。如类A所示,即使它们没有定义特定的方法,也可以使用类,例如,如果仍然可以在给定的实现中使用它们,则可以通过显式__add__调用.register方法。

如有必要,使用add.registerdiv.register定义其他类型的实现,例如:

@div.register(str)
def _(x, y):
    return x % y

之后:

>>> my_average("%s", "b") # -> `("%s" + "b") % 2`
'2b'