一、说明
Numpy的数组是强大的对象,通常用作更复杂的对象(如pandas或xarray)的基本数据结构。话虽如此,您当然也可以在自己的类中使用numpy的强大数组 - 为此,您基本上有2种方法:
- 子类方法:创建从 Numpy.ndarray 继承的类
- 容器方法:创建将数组作为属性的类
在本文中,我们将看到如何使用容器方法来包装 numpy 的数组来正确创建自己的自定义类。
二、 物理格式的项目
让我们举一个示例项目:我们想创建一个简单的项目来处理物理单位和尺寸,创建长度或重量的数组,然后使用这些数组来计算平均身高或[身体质量指数](https://en.wikipedia.org/wiki/Body_mass_index)。我们希望依靠 numpy 来完成繁重的数值计算(如加法、减法、幂),但我们也希望能够处理像 numpy 数组这样的实例,比如 or .[1, 2, 3] meter
[55 65 8] kilogram
np.sort(weights)
np.min(heights)
为此,我们将创建一个使用容器方法来包装 numpy 数组的新类。数值将存储为普通 numpy 数组,物理维度存储为字符串:
import numpy as npclass Physical():def __init__(self, value, unit=""):self.value = value # store the numerical value as a plain numpy arrayself.unit = unitdef __repr__(self):return f"<Physical:({self.value}, {self.unit})"def __str__(self):return f"{self.value} {self.unit}"weights = Physical(np.array([55.6, 45.7, 80.3]), "kilogram")
print(weights) # [55.6 45.7 80.3] kilogram
物理阵列的第一个实现
这将简单地打印:。同样,此字符串后面的数字列表是存储在 中的实际 numpy 数组。[55.6 45.7 80.3] kilogram
self.value
现在这是毫无用处的:我们不能让这个对象与其他任何东西交互,所以我们添加了基本操作,如加法或乘法与其他实例:Physical
import numpy as npclass Physical():def __init__(self, value, unit=""):self.value = valueself.unit = unitdef __repr__(self):return f"<Physical:({self.value}, {self.unit})"def __str__(self):return f"{self.value} {self.unit}"def __add__(self, other):if self.unit == other.unit:return Physical(self.value + other.value, self.unit)else:raise ValueError("Physical objects must have same unit to be added.")def __sub__(self, other):if self.unit == other.unit:return Physical(self.value - other.value, self.unit)else:raise ValueError("Physical objects must have same unit to be subtracted.")def __mul__(self, other):return Physical(self.value * other.value, f"{self.unit}*{other.unit}")def __truediv__(self, other):return Physical(self.value / other.value, f"{self.unit}/{other.unit}")def __pow__(self, powfac):return Physical(self.value**powfac, f"{self.unit}^{powfac}")weights = Physical(np.array([55.6, 45.7, 80.3]), "kilogram")
heights = Physical(np.array([1.64, 1.85, 1.77]), "meter")
print(weights) # [55.6 45.7 80.3] kilogram
print(heights) # [1.64 1.85 1.77] meter
print(heights + heights) # [3.28 3.7 3.54] meter
# print(height + weights) # raises ValueError
print(heights**2) # [2.6896 3.4225 3.1329] meter^2
现在可以将物理阵列与其他物理阵列相加或相乘。
请注意,在添加或减去物理之前,我们首先检查它们是否具有相同的单位:您不能用重量添加长度(或土豆加胡萝卜,或马用驴)。
这很好,我们现在可以计算一个身体质量指数 (BMI) 数组,给定一个以米为单位的高度数组和一个以公斤为单位的重量数组。BMI 只是通过将重量除以平方高度来给出的,即:
BMI =体重(公斤)/身高(米)^2
weights = Physical(np.array([55.6, 45.7, 80.3]), "kilogram")
heights = Physical(np.array([1.64, 1.85, 1.77]), "meter")
bmi = weights/heights**2
print(bmi) # [20.67221892 13.35281227 25.63120432] kilogram/meter^2
万岁!我们使用身高数组和身高数组计算了一个身体质量指数数组,使用后面的numpy数组来执行实际的数值计算。但是numpy的数组还有很多东西可以提供,这就是它变得非常有趣的地方。
三、实现 numpy 函数支持
Numpy 提供了许多用于数组的有用函数。仅举几例:
np.sin
等np.cos
np.tan
np.exp
等np.log
np.log10
np.add
等np.multiply
np.divide
np.min
等np.max
np.argmin
np.argmax
np.floor
等np.ceil
np.trunc
np.concatenate
等np.vstack
等等。您可以在他们的网站上找到numpy提供的所有内容:Routines — NumPy v1.25 Manual。
让我们尝试在我们的类中使用其中之一:
np.mean(bmi)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
...
<ipython-input-10-538e4626f6f7> in __truediv__(self, other)31 32 def __truediv__(self, other):
---> 33 return Physical(self._value / other._value, f"{self._unit}/{other._unit}")34 35 def __pow__(self, powfac):AttributeError: 'numpy.int64' object has no attribute '_value'
尝试调用我们的物理实例会引发一个 因为 numpy 依赖于整数的加法和除法,而我们的类没有正确实现这种操作。所以我们必须在某个地方告诉 numpy 我们想要如何表现。np.mean
bmi
AttributeError
np.mean(bmi)
这就是界面发挥作用的地方。__array_function__
该接口只是一个规范化过程,用于重载(某些)numpy 函数如何处理类中的参数。__array_function__
让我们看一个轻量级的例子来处理我们的调用:np.mean(bmi)
import numpy as np# FIRST
HANDLED_FUNCTIONS = {}class Physical():def __init__(self, value, unit=""):self._value = valueself._unit = unit# ... other methods here, see above# SECONDdef __array_function__(self, func, types, args, kwargs):if func not in HANDLED_FUNCTIONS:return NotImplemented# Note: this allows subclasses that don't override# __array_function__ to handle MyArray objectsif not all(issubclass(t, Physical) for t in types):return NotImplementedreturn HANDLED_FUNCTIONS[func](*args, **kwargs)# THIRD
def implements(numpy_function):"""Register an __array_function__ implementation for Physical objects."""def decorator(func):HANDLED_FUNCTIONS[numpy_function] = funcreturn funcreturn decorator# FOURTH
@implements(np.mean)
def np_mean_for_physical(x, *args, **kwargs):# first compute the numerical value, with no notion of unitmean_value = np.mean(x._value, *args, **kwargs)# construct a Physical instance with the result, using the same unitreturn Physical(mean_value, x._unit)weights = Physical(np.array([55.6, 45.7, 80.3]), "kilogram")
heights = Physical(np.array([1.64, 1.85, 1.77]), "meter")
bmi = weights/heights**2print(np.mean(bmi)) # 19.885411834844252 kilogram/meter^2
使用 __array_function__ 接口实现 np.mean 支持
再次欢呼,返回我们物理数组的“平均值”,这确实是一个物理量,单位为“千克/米^2”。np.mean(bmi)
让我们回顾一下我们添加到代码中以实现此目的的内容。有 4 点需要注意:
- 首先,我们在类定义之上创建一个空字典,称为 。
HANDLED_FUNCTION = {}
- 其次,我们在类中添加一个调用的方法,该方法采用一个名为 .我们将在一分钟内回到此方法的内容。
__array_function__
func
- 第三,我们创建一个装饰器构造函数:这是一个返回装饰器的函数(即,另一个将函数作为参数的函数)。我们的装饰器只是在我们的字典中创建 numpy 函数和 之间的对应关系,后者是我们版本的 numpy 函数。
implements
HANDLED_FUNCTION
func
- 第四,我们实现了 numpy 处理物理实例的平均值,当调用时是物理实例。它具有与 大致相同的签名,并执行以下操作:
np.mean(x)
x
np.mean
- 使用 x 的值计算数值平均值,这是一个普通数组。
x._value
- 然后使用平均值作为值和输入的单位作为单位创建新的物理实例。
- 最后,我们在该函数上使用装饰器。
implements
那么当我们打电话时会发生什么?np.mean(bmi)
好吧,由于 numpy 无法计算平均值,正如我们上面看到的,它检查是否有方法,并使用在 上使用的函数调用它,即 : 。bmi
__array_function__
bmi
np.mean
bmi.__array_function__(np.mean, *args, **kwargs)
由于已经在 中注册,我们用它来调用我们的版本:这里相当于 。np.mean
HANDELED_FUNCTIONS
np.mean
HANDLED_FUNCTIONS[np.mean](*args, **kwargs)
np_mean_for_physical(*args, **kwargs)
这就是如何使 numpy 的函数与您的自定义类一起工作。
不幸的是,这并不完全正确。此接口仅适用于某些 numpy 函数,但不适用于全部。
还记得上面的功能列表吗?好吧,我们可以将它们分为 2 个子列表:常规 numpy 函数和 numpy 通用函数 — 或简称“ufuncs”:
- Numpy 函数 : , , , , ,
np.min
np.max
np.argmin
np.argmax
np.concatenate
np.vstack.
- Numpy ufuncs : , , ,, , ,, , , , ,
np.sin
np.cos
np.tan
np.exp
np.log
np.log10
np.add
np.multiply
np.divide
np.floor
np.ceil
np.trunc
我们看到了如何使用 实现 numpy 函数支持。在下一篇文章中,我们现在将看到如何使用该接口添加对“ufuncs”的支持。__array_function__
__array_ufunc__
四、总结一下
- 使用 numpy 数组的容器方法包括在自定义类实例中将数组设置为属性(与子类化数组相反)。
- 要使类使用 numpy 函数调用(如 ),必须在类中实现接口。
np.mean(my_array_like_instance)
__array_function__
- 这基本上是通过向类添加一个方法,编写自己的包装器(就像我们对 ) 所做的那样,并将它们链接在一起(就像我们对查找字典所做的那样)。
__array_function__
np_mean_for_physical
HANDLED_FUNCTIONS
- 请注意,这仅适用于“常规”numpy 函数。对于numpy的“通用”函数,您还需要实现该接口。
__array_ufunc__
这个主题相当广泛,所以这里有几个链接,你应该阅读,以更好地掌握利害关系:
- 容器方法:Writing custom array containers — NumPy v1.25 Manual
__array_function__
参考资料: Standard array subclasses — NumPy v1.25 Manual- 参考资料: Universal functions (ufunc) — NumPy v1.25 Manual
以下是我们在本文中编写的完整代码:
import numpy as npHANDLED_FUNCTIONS = {}class Physical():def __init__(self, value, unit=""):self._value = valueself._unit = unitdef __repr__(self):return f"<Physical:({self._value}, {self._unit})"def __str__(self):return f"{self._value} {self._unit}"def __add__(self, other):if self._unit == other._unit:return Physical(self._value + other._value, self._unit)else:raise ValueError("Physical objects must have same unit to be added.")def __sub__(self, other):if self._unit == other._unit:return Physical(self._value - other._value, self._unit)else:raise ValueError("Physical objects must have same unit to be subtracted.")def __mul__(self, other):return Physical(self._value * other._value, f"{self._unit}*{other._unit}")def __truediv__(self, other):return Physical(self._value / other._value, f"{self._unit}/{other._unit}")def __pow__(self, powfac):return Physical(self._value**powfac, f"{self._unit}^{powfac}")def __array_function__(self, func, types, args, kwargs):if func not in HANDLED_FUNCTIONS:return NotImplemented# Note: this allows subclasses that don't override# __array_function__ to handle Physical objectsif not all(issubclass(t, Physical) for t in types):return NotImplementedreturn HANDLED_FUNCTIONS[func](*args, **kwargs)def implements(numpy_function):"""Register an __array_function__ implementation for Physical objects."""def decorator(func):HANDLED_FUNCTIONS[numpy_function] = funcreturn funcreturn decorator@implements(np.mean)
def np_mean_for_physical(x, *args, **kwargs):# first compute the numerical value, with no notion of unitmean_value = np.mean(x._value, *args, **kwargs)# construct a Physical instance with the result, using the same unitreturn Physical(mean_value, x._unit)weights = Physical(np.array([55.6, 45.7, 80.3]), "kilogram")
heights = Physical(np.array([1.64, 1.85, 1.77]), "meter")
print(weights)
print(heights)
print(heights + heights)
print(heights**2)
ratio = weights/heights
print(ratio)
bmi = weights/heights**2
print(bmi)
print(np.mean(bmi))
干杯!