标准数组子类#
注意
子类化 numpy.ndarray
是可行的,但如果你的目标是创建一个行为已修改的数组,例如用于分布式计算的 dask 数组和用于 GPU 计算的 cupy 数组,则不建议使用子类化。相反,建议使用 NumPy 的调度机制。
如果需要,可以继承 ndarray
(在 Python 或 C 中)。因此,它可以为许多有用的类奠定基础。通常,是子类化数组对象还是仅将核心数组组件用作新类的内部部分是一个困难的决定,这可能仅仅是个人选择的问题。NumPy 提供了多种工具来简化新对象与其他数组对象的交互方式,因此最终的选择可能并不重要。简化这个问题的一种方法是问自己,你感兴趣的对象是否可以被单个数组替换,或者它是否真的需要两个或更多数组作为其核心。
请注意,asarray
总是返回基类 ndarray。如果你确信你的数组对象使用可以处理 ndarray 的任何子类,那么可以使用 asanyarray
允许子类更清晰地通过你的子例程传播。原则上,子类可以重新定义数组的任何方面,因此,在严格的指导下,asanyarray
很少有用。然而,数组对象的大多数子类不会重新定义数组对象的某些方面,例如缓冲区接口或数组的属性。然而,你的子例程可能无法处理数组的任意子类的一个重要例子是,矩阵将“*”运算符重新定义为矩阵乘法,而不是元素逐元素乘法。
特殊属性和方法#
另请参阅
NumPy 提供了几个类可以自定义的钩子
- class.__array_ufunc__(ufunc, method, *inputs, **kwargs)#
任何类,无论是否为 ndarray 子类,都可以定义此方法或将其设置为
None
,以覆盖 NumPy ufuncs 的行为。这与 Python 的__mul__
和其他二元操作例程的工作方式非常相似。ufunc 是被调用的 ufunc 对象。
method 是一个字符串,指示调用了哪个 Ufunc 方法(
"__call__"
、"reduce"
、"reduceat"
、"accumulate"
、"outer"
、"inner"
之一)。inputs 是
ufunc
的输入参数元组。kwargs 是一个字典,包含 ufunc 的可选输入参数。如果给定,任何
out
参数,包括位置参数和关键字参数,都作为tuple
在 kwargs 中传递。详见通用函数 (ufunc) 中的讨论。
如果请求的操作未实现,该方法应返回操作结果或
NotImplemented
。如果输入、输出或
where
参数之一具有__array_ufunc__
方法,则会执行此方法而不是 ufunc。如果多个参数实现了__array_ufunc__
,它们将按以下顺序尝试:子类先于超类,输入先于输出,输出先于where
,否则从左到右。第一个返回除NotImplemented
之外值的例程将确定结果。如果所有__array_ufunc__
操作都返回NotImplemented
,则会引发TypeError
。注意
我们打算将 NumPy 函数重新实现为(广义)Ufunc,在这种情况下,它们将可以通过
__array_ufunc__
方法被覆盖。一个主要候选是matmul
,它目前不是 Ufunc,但可以相对容易地重写为(一组)广义 Ufunc。同样的情况也可能发生在median
、amin
和argsort
等函数上。与 Python 中的其他一些特殊方法(例如
__hash__
和__iter__
)一样,可以通过设置__array_ufunc__ = None
来表明你的类不支持 ufuncs。当对设置了__array_ufunc__ = None
的对象调用 ufuncs 时,总是会引发TypeError
。__array_ufunc__
的存在也影响了ndarray
在arr
是ndarray
且obj
是自定义类的实例时,如何处理诸如arr + obj
和arr < obj
之类的二元操作。有两种可能性。如果obj.__array_ufunc__
存在且不为None
,那么ndarray.__add__
及相关方法将委托给 ufunc 机制,这意味着arr + obj
变为np.add(arr, obj)
,然后add
调用obj.__array_ufunc__
。如果你想定义一个行为类似于数组的对象,这很有用。另一种情况是,如果
obj.__array_ufunc__
设置为None
,那么作为特殊情况,像ndarray.__add__
这样的特殊方法会注意到这一点,并无条件地引发TypeError
。如果你想创建通过二元操作与数组交互但本身不是数组的对象,这很有用。例如,一个单位处理系统可能有一个表示“米”单位的对象m
,并希望支持语法arr * m
来表示数组的单位是“米”,但不希望通过 ufuncs 或其他方式与数组交互。这可以通过设置__array_ufunc__ = None
并定义__mul__
和__rmul__
方法来完成。(请注意,这意味着编写一个总是返回NotImplemented
的__array_ufunc__
与设置__array_ufunc__ = None
不完全相同:在前一种情况下,arr + obj
将引发TypeError
,而在后一种情况下,可以定义__radd__
方法来防止这种情况。)上述情况不适用于原地操作符,对于原地操作符,
ndarray
从不返回NotImplemented
。因此,arr += obj
总是会导致TypeError
。这是因为对于数组,原地操作通常不能简单地被一个反向操作替换。(例如,默认情况下,arr += obj
将被转换为arr = arr + obj
,即arr
将被替换,这与原地数组操作的预期不符。)注意
如果你定义
__array_ufunc__
如果你不是
ndarray
的子类,我们建议你的类定义特殊方法,如__add__
和__lt__
,这些方法像 ndarray 一样委托给 ufuncs。实现这一点的一个简单方法是继承自NDArrayOperatorsMixin
。如果你子类化
ndarray
,我们建议你将所有覆盖逻辑放在__array_ufunc__
中,而不要同时覆盖特殊方法。这确保了类层次结构仅在一个地方确定,而不是由 ufunc 机制和二元操作规则(优先考虑子类的特殊方法)分开确定(强制单点层次结构的另一种方法,即将__array_ufunc__
设置为None
,会显得非常出乎意料,因此令人困惑,因为那样子类将根本无法与 ufuncs 一起工作)。ndarray
定义了自己的__array_ufunc__
,如果没有参数具有覆盖,它将评估 ufunc,否则返回NotImplemented
。这对于子类可能很有用,因为子类的__array_ufunc__
会将其自身类的任何实例转换为ndarray
:然后它可以使用super().__array_ufunc__(*inputs, **kwargs)
将这些传递给其超类,最后在可能的回溯转换后返回结果。这种做法的优点是确保可以有一个扩展行为的子类层次结构。详见子类化 ndarray。
- class.__array_function__(func, types, args, kwargs)#
func
是 NumPy 公共 API 暴露的任意可调用对象,以func(*args, **kwargs)
的形式被调用。types
是一个集合collections.abc.Collection
,包含原始 NumPy 函数调用中实现__array_function__
的唯一参数类型。元组
args
和字典kwargs
直接从原始调用中传递。
为了方便
__array_function__
的实现者,types
提供了所有带有'__array_function__'
属性的参数类型。这允许实现者快速识别何时应将处理委托给其他参数上的__array_function__
实现。实现不应依赖于types
的迭代顺序。大多数
__array_function__
的实现将从两个检查开始给定的函数是我们知道如何重载的吗?
所有参数都是我们知道如何处理的类型吗?
如果这些条件成立,
__array_function__
应该返回对其func(*args, **kwargs)
实现的调用结果。否则,它应该返回哨兵值NotImplemented
,表明这些类型未实现该函数。对
__array_function__
的返回值没有一般性要求,尽管大多数合理的实现可能应该返回与函数某个参数类型相同的数组。定义自定义装饰器(下面的
implements
)来注册__array_function__
实现也可能很方便。HANDLED_FUNCTIONS = {} class MyArray: 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 MyArray objects if not all(issubclass(t, MyArray) for t in types): return NotImplemented return HANDLED_FUNCTIONS[func](*args, **kwargs) def implements(numpy_function): """Register an __array_function__ implementation for MyArray objects.""" def decorator(func): HANDLED_FUNCTIONS[numpy_function] = func return func return decorator @implements(np.concatenate) def concatenate(arrays, axis=0, out=None): ... # implementation of concatenate for MyArray objects @implements(np.broadcast_to) def broadcast_to(array, shape): ... # implementation of broadcast_to for MyArray objects
请注意,
__array_function__
实现不要求包含所有相应的 NumPy 函数的可选参数(例如,上面的broadcast_to
省略了不相关的subok
参数)。可选参数仅在 NumPy 函数调用中明确使用时才传递给__array_function__
。就像
__add__
等内置特殊方法一样,正确编写的__array_function__
方法在遇到未知类型时应始终返回NotImplemented
。否则,如果操作也包含你的某个对象,将无法从另一个对象正确覆盖 NumPy 函数。大部分情况下,使用
__array_function__
的调度规则与__array_ufunc__
的调度规则相符。具体来说:NumPy 将从所有指定的输入中收集
__array_function__
的实现,并按顺序调用它们:子类优先于超类,否则从左到右。请注意,在某些涉及子类的边缘情况下,这与 Python 的当前行为略有不同。__array_function__
的实现通过返回除NotImplemented
之外的任何值来表示它们可以处理该操作。如果所有
__array_function__
方法都返回NotImplemented
,NumPy 将引发TypeError
。
如果不存在
__array_function__
方法,NumPy 将默认调用其自己的实现,该实现旨在用于 NumPy 数组。例如,当所有类数组参数都是 Python 数字或列表时,就会出现这种情况。(NumPy 数组确实有一个__array_function__
方法,如下所示,但如果除了 NumPy 数组子类之外的任何参数实现了__array_function__
,它总是返回NotImplemented
。)与
__array_ufunc__
当前行为的一个不同之处在于,NumPy 只会在每个唯一类型的第一个参数上调用__array_function__
。这与 Python 的调用反射方法规则相符,并且确保即使存在大量重载参数时,检查重载也具有可接受的性能。
- class.__array_finalize__(obj)#
每当系统从 obj 内部分配一个新数组时,就会调用此方法,其中 obj 是
ndarray
的一个子类(子类型)。它可用于在构造后更改 self 的属性(例如,以确保一个二维矩阵),或从“父级”更新元信息。子类继承了此方法的默认实现,该实现不执行任何操作。
- class.__array_wrap__(array, context=None, return_scalar=False)#
在每个 ufunc 结束时,此方法会在具有最高数组优先级的输入对象上调用,如果指定了输出对象,则会在输出对象上调用。ufunc 计算的数组会作为参数传入,并且返回的任何内容都会传递给用户。子类继承了此方法的默认实现,该实现将数组转换为对象类的新实例。子类可以选择使用此方法将输出数组转换为子类实例,并在将数组返回给用户之前更新元数据。
NumPy 也可以从非 ufuncs 调用此函数而无需上下文,以允许保留子类信息。
2.0 版本中更改: 现在
return_scalar
作为False
(通常)或True
传递,指示 NumPy 是否会返回标量。子类可以忽略此值,或返回array[()]
以使其行为更像 NumPy。注意
希望最终废弃此方法,转而支持用于 ufuncs 的
__array_ufunc__
(以及用于少数其他函数如numpy.squeeze
的__array_function__
)。
- class.__array_priority__#
此属性的值用于确定在返回对象的 Python 类型有多种可能性时,应返回哪种类型的对象。子类继承此属性的默认值为 0.0。
注意
对于 ufuncs,希望最终废弃此方法,转而支持
__array_ufunc__
。
- class.__array__(dtype=None, copy=None)#
如果在对象上定义了此方法,它必须返回一个 NumPy
ndarray
。如果将实现此接口的对象传递给np.array()
等数组强制转换函数,则会调用此方法。__array__
的第三方实现必须接受dtype
和copy
参数。自 NumPy 2.0 版本弃用: 自 NumPy 2 起,不实现
copy
和dtype
已被弃用。添加它们时,你必须确保copy
的行为正确。dtype
是返回数组的请求数据类型,由 NumPy 按位置传递(仅在用户请求时)。可以忽略dtype
,因为 NumPy 会检查结果并在必要时将其转换为dtype
。如果更有效地将数据强制转换为请求的 dtype 而不依赖 NumPy,你应在你的库中处理它。copy
是一个通过关键字传递的布尔值。如果copy=True
,你必须返回一个副本。返回现有数据的视图将导致用户代码错误。如果copy=False
,则用户请求永不创建副本,你必须引发错误,除非没有创建副本且返回的数组是现有数据的视图。对于copy=False
始终引发错误是有效的。默认值copy=None
(未传递)允许结果是视图或副本。但是,在可能的情况下,应首选返回视图。
请参阅与 NumPy 的互操作性以了解协议层次结构,其中
__array__
是最旧且最不理想的。
矩阵对象#
注意
强烈建议不要使用 matrix 子类。如下所述,它使得编写一致处理矩阵和常规数组的函数变得非常困难。目前,它们主要用于与 scipy.sparse
交互。然而,我们希望为此用例提供替代方案,并最终移除 matrix
子类。
matrix
对象继承自 ndarray,因此它们具有 ndarray 的相同属性和方法。然而,矩阵对象有六个重要区别,当你使用矩阵但期望它们像数组一样操作时,这些区别可能会导致意外结果
矩阵对象可以使用字符串表示法创建,以允许 Matlab 风格的语法,其中空格分隔列,分号(‘;’)分隔行。
矩阵对象总是二维的。这具有深远的影响,因为 m.ravel() 仍然是二维的(第一维为 1),并且项目选择返回二维对象,因此序列行为与数组根本不同。
矩阵对象会覆盖乘法运算符,使其成为矩阵乘法。请务必理解这一点,特别是对于你可能希望接收矩阵的函数。鉴于当 m 是矩阵时 asanyarray(m) 返回矩阵这一事实,这一点尤为重要。
矩阵对象会覆盖幂运算符,使其成为矩阵的幂。对于在函数中使用 asanyarray(…) 获取数组对象时使用幂运算,同样适用上述警告。
矩阵对象的默认
__array_priority__
为 10.0,因此与 ndarray 的混合操作总是产生矩阵。矩阵具有使计算更简单的特殊属性。它们是:
警告
矩阵对象会覆盖乘法运算符“*”和幂运算符“**”,使其分别成为矩阵乘法和矩阵幂。如果你的子例程可以接受子类并且你不转换为基类数组,那么你必须使用 ufuncs 的 multiply 和 power 方法来确保你对所有输入执行了正确的操作。
matrix 类是 ndarray 的 Python 子类,可以作为如何构建自己的 ndarray 子类的参考。矩阵可以从其他矩阵、字符串以及任何可以转换为 ndarray
的对象创建。在 NumPy 中,“mat”是“matrix”的别名。
|
从类数组对象或数据字符串返回一个矩阵。 |
|
将输入解释为矩阵。 |
|
从字符串、嵌套序列或数组构建一个矩阵对象。 |
示例 1:从字符串创建矩阵
>>> import numpy as np
>>> a = np.asmatrix('1 2 3; 4 5 3')
>>> print((a*a.T).I)
[[ 0.29239766 -0.13450292]
[-0.13450292 0.08187135]]
示例 2:从嵌套序列创建矩阵
>>> import numpy as np
>>> np.asmatrix([[1,5,10],[1.0,3,4j]])
matrix([[ 1.+0.j, 5.+0.j, 10.+0.j],
[ 1.+0.j, 3.+0.j, 0.+4.j]])
示例 3:从数组创建矩阵
>>> import numpy as np
>>> np.asmatrix(np.random.rand(3,3)).T
matrix([[4.17022005e-01, 3.02332573e-01, 1.86260211e-01],
[7.20324493e-01, 1.46755891e-01, 3.45560727e-01],
[1.14374817e-04, 9.23385948e-02, 3.96767474e-01]])
内存映射文件数组#
内存映射文件对于读写/修改具有规则布局的大文件的少量片段非常有用,而无需将整个文件读入内存。ndarray 的一个简单子类使用内存映射文件作为数组的数据缓冲区。对于小文件,将整个文件读入内存的开销通常不显著,但对于大文件,使用内存映射可以节省大量资源。
内存映射文件数组除了继承自 ndarray 的方法外,还多了一个方法:.flush()
,用户必须手动调用它以确保对数组的任何更改都实际写入磁盘。
|
创建到存储在磁盘上的二进制文件中的数组的内存映射。 |
将数组中的任何更改写入磁盘文件。 |
示例
>>> import numpy as np
>>> a = np.memmap('newfile.dat', dtype=float, mode='w+', shape=1000)
>>> a[10] = 10.0
>>> a[30] = 30.0
>>> del a
>>> b = np.fromfile('newfile.dat', dtype=float)
>>> print(b[10], b[30])
10.0 30.0
>>> a = np.memmap('newfile.dat', dtype=float)
>>> print(a[10], a[30])
10.0 30.0
字符数组 (numpy.char
)#
另请参阅
注意
chararray
类的存在是为了与 Numarray 向后兼容,不建议用于新开发。从 NumPy 1.4 开始,如果需要字符串数组,建议使用 dtype
为 object_
、bytes_
或 str_
的数组,并使用 numpy.char
模块中的自由函数进行快速向量化字符串操作。
这些是 str_
类型或 bytes_
类型的增强型数组。这些数组继承自 ndarray
,但特别定义了 +
、*
和 %
操作,使其基于(广播)元素逐个执行。这些操作在字符类型的标准 ndarray
上不可用。此外,chararray
具有所有标准的 str
(和 bytes
)方法,并以元素逐个的方式执行它们。创建 chararray 最简单的方法可能是使用 self.view(chararray)
,其中 self 是 str 或 unicode 数据类型的 ndarray。然而,chararray 也可以通过 chararray
构造函数或通过 numpy.char.array
函数创建
|
提供字符串和 Unicode 值数组的便捷视图。 |
|
创建一个 |
与标准 str 数据类型的 ndarray 另一个不同之处是,chararray 继承了 Numarray 引入的特性,即数组中任何元素的末尾空白字符在项检索和比较操作时将被忽略。
记录数组#
另请参阅
NumPy 提供了 recarray
类,该类允许将结构化数组的字段作为属性访问,以及相应的标量数据类型对象 record
。
注意
pandas DataFrame 比记录数组更强大。如果可能,请改用 pandas DataFrame。
掩码数组 (numpy.ma
)#
另请参阅
标准容器类#
为了向后兼容并作为一个标准的“容器”类,Numeric 中的 UserArray 已被引入 NumPy 并命名为 numpy.lib.user_array.container
。container 类是一个 Python 类,其 self.array 属性是一个 ndarray。使用 numpy.lib.user_array.container 进行多重继承可能比直接使用 ndarray 本身更容易,因此它被默认包含。除了提及它的存在之外,这里没有对其进行文档说明,因为如果可能,鼓励你直接使用 ndarray 类。
|
用于轻松实现多重继承的标准容器类。 |
数组迭代器#
迭代器是数组处理中的一个强大概念。本质上,迭代器实现了广义的 for 循环。如果 myiter 是一个迭代器对象,那么 Python 代码
for val in myiter:
...
some code involving val
...
重复调用 val = next(myiter)
,直到迭代器引发 StopIteration
。有几种遍历数组的方法可能有用:默认迭代、扁平迭代和 \(N\) 维枚举。
默认迭代#
ndarray 对象的默认迭代器是序列类型的默认 Python 迭代器。因此,当数组对象本身用作迭代器时。默认行为等同于
for i in range(arr.shape[0]):
val = arr[i]
此默认迭代器从数组中选择维度为 \(N-1\) 的子数组。这对于定义递归算法非常有用。遍历整个数组需要 \(N\) 个 for 循环。
>>> import numpy as np
>>> a = np.arange(24).reshape(3,2,4) + 10
>>> for val in a:
... print('item:', val)
item: [[10 11 12 13]
[14 15 16 17]]
item: [[18 19 20 21]
[22 23 24 25]]
item: [[26 27 28 29]
[30 31 32 33]]
扁平迭代#
数组的 1-D 迭代器。 |
如前所述,ndarray 对象的 flat 属性返回一个迭代器,它将以 C 风格的连续顺序遍历整个数组。
>>> import numpy as np
>>> for i, val in enumerate(a.flat):
... if i%5 == 0: print(i, val)
0 10
5 15
10 20
15 25
20 30
在这里,我使用了内置的 enumerate 迭代器来返回迭代器索引和值。
N 维枚举#
|
多维索引迭代器。 |
有时,在迭代时获取 N 维索引可能很有用。ndenumerate 迭代器可以实现这一点。
>>> import numpy as np
>>> for i, val in np.ndenumerate(a):
... if sum(i)%5 == 0:
print(i, val)
(0, 0, 0) 10
(1, 1, 3) 25
(2, 0, 3) 29
(2, 1, 2) 32
广播迭代器#
生成一个模仿广播的对象。 |
广播的一般概念也可以通过 Python 使用 broadcast
迭代器获得。此对象接受 \(N\) 个对象作为输入,并返回一个迭代器,该迭代器返回元组,提供广播结果中每个输入序列元素。
>>> import numpy as np
>>> for val in np.broadcast([[1, 0], [2, 3]], [0, 1]):
... print(val)
(np.int64(1), np.int64(0))
(np.int64(0), np.int64(1))
(np.int64(2), np.int64(0))
(np.int64(3), np.int64(1))