我有一个问题要问 NumPy 专家。考虑一个 NumPy 标量:
c = np.arange(3.0).sum()
如果我尝试将它与自定义序列相乘,例如
class S:
def __init__(self, lst):
self.lst = lst
def __len__(self):
return len(self.lst)
def __getitem__(self, s):
return self.lst[s]
c = np.arange(3.0).sum()
s = S([1, 2, 3])
print(c * s)
,它会起作用并且我得到:
array([3., 6., 9.])
但是,我不能用列表来做到这一点。例如,如果我从 list 继承 S 并尝试它,这不再起作用
class S(list):
def __init__(self, lst):
self.lst = lst
def __len__(self):
return len(self.lst)
def __getitem__(self, s):
return self.lst[s]
c = np.arange(3.0).sum()
s = S([1, 2, 3])
print(c * s)
并且我得到“无法将序列乘以‘numpy.float64’类型的非 int”。
那么 NumPy 是如何做到的区分这两种情况?
我问是因为我想防止我的“S”类出现这种行为而不从列表继承。
这是一个关于 NumPy 如何与其自身以外的对象交互的好问题。
答案在于 NumPy 在执行乘法等操作时如何处理 Python 对象。当使用
*
运算符时,NumPy 不会自动将的自定义类转换为 NumPy 数组。相反,它会依赖于
鸭子类型
和
运算符重载
。
在的第一个示例中,
class S
定义了
__len__
和
__getitem__
。这些方法允许 NumPy 像序列一样对待
S
的实例。NumPy 本质上会遍历
S
,将标量值
c
乘以从
__getitem__
返回的每个元素。
但是,当从
list
继承
class S
时,会引入 NumPy 已经知道如何处理的行为。列表有自己的
*
运算符实现,用于
标量
,它只执行列表的重复(这就是为什么收到错误的原因,因为它无法将浮点数乘以列表)。由于继承自
list
,NumPy 会默认使用列表的行为,从而导致冲突。
要防止这种行为而不从
list
继承,可以实现
__rmul__
特殊方法。此方法控制当 NumPy 标量位于右侧时会发生什么。以下是修改后的代码:
import numpy as np
class S:
def __init__(self, lst):
self.lst = lst
def __len__(self):
return len(self.lst)
def __getitem__(self, s):
return self.lst[s]
def __rmul__(self, other):
return NotImplemented
c = np.arange(3.0).sum()
s = S([1, 2, 3])
print(c * s) # 这将引发 TypeError
通过从
__rmul__
返回
NotImplemented
,表示的类不支持此特定操作,从而导致
TypeError
,类似于在尝试将列表与 NumPy 浮点数相乘时获得的错误。
此修改可确保的
S
类与 NumPy 标量的交互一致,无论它是否继承自
list
。