如何使我的 class 对 operator/function 重载更稳健?

How can I make my class more robust to operator/function overloading?

我正在写一个class,其中对象用两个参数(ab)初始化。目的是将此 class 的实例分配给变量,以便我可以用 Python 代码符号化地编写一个等式,但让运算符重载对 a 和 [=16= 执行独特的操作].

import numpy as np


class my_class(object):

    def __init__(self, a, b):
        self.value1 = a
        self.value2 = b

    # Example of an overloaded operator that works just fine
    def __mul__(self, other):
        new_a = self.value1 * other
        new_b = self.value2 * np.absolute(other)
        return my_class(new_a, new_b)


if __name__ == "__main__":
    my_object = my_class(100, 1)

    print(np.exp(my_object))    # This doesn't work!

在运行上面的示例代码中,我遇到了如下输出:

TypeError: loop of ufunc does not support argument 0 of type my_class which has no callable exp method

通过猜测,我能够看到关于 no callable exp method 的投诉可能意味着我需要在我的 class:

中定义一个新方法
def exp(self):
    new_val1 = np.exp(self.value1)
    new_val2 = np.absolute(new_val1) * self.value2
    return my_class(new_val1, new_val2)

最终运行良好。但是现在我将不得不根据需要为 np.expm1() 等编写另一个方法。谢天谢地,我只需要 np.exp()np.log() 就可以工作,但我也在我的对象上尝试了 math.exp(),但我开始出现类型错误。

所以现在我的问题是: class 中的自定义 exp 方法似乎适用于 重载 NumPy 函数,但我应该如何处理 math.exp() 不工作?这一定是可能的,因为在 NumPy 数组上调用 math.exp() 时,NumPy 知道可以将 1 元素数组转换为标量,然后毫无问题地传递给 math.exp()。 我的意思是我猜这在技术上是关于重载一个函数,但在我意识到定义一个新的 exp 是解决我的第一个问题之前,我不知道为什么像 __rpow__ 这样的方法没有被调用.

np.exp(my_object) 实现为 np.exp(np.array(my_object)).

np.array(my_object) 是一个对象数据类型数组。 np.exp 对数组的每个元素尝试 elmt.exp()。这对大多数 classes 不起作用,因为它们没有实现这样的方法。

同样适用于运算符和其他 ufunc

math.exp 是一个不相关的实现。它显然适用于提供单个数值的东西,但我还没有探索那么多。 numpy 如果不能这样做,将引发错误。

用 class __mul__ 实现 * 由解释器完成。


math.exp__float__()

中使用数组时出现相同的错误消息
In [52]: math.exp(np.array([1,2,3]))
Traceback (most recent call last):
  File "<ipython-input-52-40503a52084a>", line 1, in <module>
    math.exp(np.array([1,2,3]))
TypeError: only size-1 arrays can be converted to Python scalars

In [53]: np.array([1,2,3]).__float__()
Traceback (most recent call last):
  File "<ipython-input-53-0bacdf9df4e7>", line 1, in <module>
    np.array([1,2,3]).__float__()
TypeError: only size-1 arrays can be converted to Python scalars

类似地,当在布尔上下文中使用数组时(例如 if),我们可以通过

生成错误
In [55]: np.array([1,2,3]).__bool__()
Traceback (most recent call last):
  File "<ipython-input-55-04aca1612817>", line 1, in <module>
    np.array([1,2,3]).__bool__()
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

类似地,在 if 中使用 sympy 关系会导致

产生错误
In [110]: (x>0).__bool__()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-110-d88b76ce6b22> in <module>
----> 1 (x>0).__bool__()

/usr/local/lib/python3.8/dist-packages/sympy/core/relational.py in __bool__(self)
    396 
    397     def __bool__(self):
--> 398         raise TypeError("cannot determine truth value of Relational")
    399 
    400     def _eval_as_set(self):

TypeError: cannot determine truth value of Relational

pandas 系列产生类似的错误。

首先,为什么 math.exp 使用 1 元素 Numpy 数组:numpy.ndarray class has a __float__ method; this method is part of the Python data model for numeric types, and is used when float(x) is called. I couldn't spot anything in the math docs 表示 math.exp 将其参数转换为float,但这并不是不合理的行为。

至于 Numpy 的 ufunc 的自定义行为:实现覆盖 ufunc 行为的“类数组”对象的推荐方法是 somewhat complicated。我找不到有关提供 explog 等自定义 ufunc 方法的文档。像这样提供方法并非在所有情况下都有效,例如 np.heaviside;这个例子

import numpy as np


class foo:
    
    def exp(self):
        return foo()

    def heaviside(self, other):
        return foo()


print(f'{np.exp(foo()) = }')
print(f'{np.heaviside(foo(), foo()) = }')

给出此输出:

np.exp(foo()) = <__main__.foo object at 0x7efcdbba1ac0>
Traceback (most recent call last):
  File "/home/rory/hack/Whosebug/q70312146/heaviside.py", line 14, in <module>
    print(f'{np.heaviside(foo(), foo()) = }')
TypeError: ufunc 'heaviside' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''