标准数组子类#

注意

可以对 numpy.ndarray 进行子类化,但如果您的目标是创建具有修改后行为的数组,例如用于分布式计算的 dask 数组和用于 GPU 计算的 cupy 数组,则不建议进行子类化。而是推荐使用 NumPy 的 分派机制

如果需要,可以继承 ndarray(在 Python 或 C 中)。因此,它可以为许多有用的类奠定基础。通常,是否对数组对象进行子类化,还是仅仅将核心数组组件用作新类的内部部分,这是一个困难的决定,也可能仅仅是选择问题。NumPy 提供了多种工具来简化新对象与其他数组对象的交互方式,因此最终选择可能并不重要。简化此问题的一种方法是问自己您感兴趣的对象是否可以替换为单个数组,或者它是否确实需要两个或更多数组作为其核心。

请注意,asarray 始终返回基类 ndarray。如果您确信您对数组对象的使用可以处理 ndarray 的任何子类,那么可以使用 asanyarray 来允许子类更干净地传播到您的子程序中。原则上,子类可以重新定义数组的任何方面,因此,在严格的指导方针下,asanyarray 很少有用。但是,大多数数组对象的子类不会重新定义数组对象的某些方面,例如缓冲区接口或数组的属性。然而,一个重要的例子说明了为什么您的子程序可能无法处理任意的数组子类是,矩阵将“*”运算符重新定义为矩阵乘法,而不是逐元素乘法。

特殊属性和方法#

另请参阅

子类化 ndarray

NumPy 提供了几个类可以定制的钩子

class.__array_ufunc__(ufunc, method, *inputs, **kwargs)#

任何类,无论是 ndarray 的子类还是非子类,都可以定义此方法或将其设置为 None,以覆盖 NumPy ufunc 的行为。这与 Python 的 __mul__ 和其他二元运算例程的工作方式非常相似。

  • ufunc 是被调用的 ufunc 对象。

  • method 是一个字符串,指示调用了哪个 Ufunc 方法(是 "__call__""reduce""reduceat""accumulate""outer""inner" 之一)。

  • inputsufunc 的输入参数的元组。

  • kwargs 是包含 ufunc 可选输入参数的字典。如果给出,任何 out 参数(位置参数和关键字参数)都将作为 元组 传递到 kwargs 中。有关详细信息,请参阅 通用函数 (ufunc) 中的讨论。

该方法应返回操作的结果,或在操作未实现时返回 NotImplemented

如果输入、输出或 where 参数中的一个具有 __array_ufunc__ 方法,则会代替 ufunc 执行它。如果多个参数实现了 __array_ufunc__,则按以下顺序尝试:子类优先于超类,输入优先于输出,输出优先于 where,否则从左到右。第一个返回非 NotImplemented 的例程确定结果。如果所有 __array_ufunc__ 操作都返回 NotImplemented,则会引发 TypeError

注意

我们打算将 NumPy 函数重新实现为(广义)Ufunc,在这种情况下,它们将可以通过 __array_ufunc__ 方法进行覆盖。一个主要候选者是 matmul,它目前不是 Ufunc,但可以相对容易地重写为(一组)广义 Ufunc。同样的情况也可能发生在 medianaminargsort 等函数上。

与 Python 中的其他一些特殊方法(如 __hash____iter__)一样,可以通过将 __array_ufunc = None 来指示您的类不支持 ufuncs。当对设置了 __array_ufunc = None 的对象调用 ufuncs 时,它们总是引发 TypeError

是否存在 __array_ufunc__ 还会影响当 arrndarrayobj 是自定义类的实例时,ndarray 如何处理 arr + objarr < 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__ 方法来防止这种情况。)

上述情况不适用于原地(in-place)运算符,对于原地运算符,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__ 的实现都将从两个检查开始

  1. 给定的函数是否是我们知道如何重载的?

  2. 所有参数是否都是我们知道如何处理的类型?

如果满足这些条件,__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_ufunc__ 的分派规则,__array_function__ 大致相同。特别是

  • 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(其中objndarray 的子类(子类型))内部分配新数组时,都会调用此方法。它可以用于在构造后更改self的属性(例如,确保一个 2D 矩阵),或从“父”对象更新元信息。子类继承此方法的默认实现,该实现不执行任何操作。

class.__array_wrap__(array, context=None, return_scalar=False)#

在每次 ufunc 结束时,都会使用数组优先级最高的输入对象或指定的输出对象调用此方法。ufunc 计算出的数组被传入,返回的任何内容都传递给用户。子类继承此方法的默认实现,该实现将数组转换为对象类的新实例。子类可以选择使用此方法将输出数组转换为子类的实例,并在将数组返回给用户之前更新元数据。

NumPy 也可能在没有上下文的情况下调用此函数,以允许保留子类信息。

版本 2.0 中已更改: 现在 return_scalarFalse(通常)或 True 的形式传递,表示 NumPy 将返回标量。子类可以忽略该值,或返回 array[()] 以更像 NumPy。

注意

希望最终将此方法弃用,转而使用 __array_ufunc__ 来处理 ufuncs(以及 __array_function__ 来处理 numpy.squeeze 等其他一些函数)。

class.__array_priority__#

此属性的值用于确定在返回对象的 Python 类型有多种可能性时应返回哪种类型的对象。子类继承此属性的默认值 0.0。

注意

对于 ufuncs,希望最终将此方法弃用,转而使用 __array_ufunc__

class.__array__(dtype=None, copy=None)#

如果定义在对象上,则必须返回 NumPy ndarray。当将实现此接口的对象传递给 np.array() 等数组强制转换函数时,会调用此方法。

第三方实现 __array__ 必须接受 dtypecopy 参数。

NumPy 版本 2.0 已弃用: 不实现 copydtype 自 NumPy 2.0 起已弃用。添加它们时,必须确保 copy 的行为正确。

  • dtype 是所请求的返回数组的数据类型,由 NumPy 按位置传递(仅当用户请求时)。忽略 dtype 是可以接受的,因为 NumPy 会检查结果并在必要时强制转换为 dtype。如果将数据强制转换为请求的 dtype 而不依赖于 NumPy 更有效,则应在您的库中处理。

  • copy 是一个通过关键字传递的布尔值。如果 copy=True,您必须返回一个副本。返回现有数据的视图将导致用户代码错误。如果 copy=False,用户请求永不创建副本,您必须引发错误,除非未创建副本且返回的数组是现有数据的视图。对于 copy=False,始终引发错误是有效的。copy=None(未传递)的默认值允许结果既可以是视图也可以是副本。但是,如果可能,应优先返回视图。

有关协议层次结构,请参阅 与 NumPy 的互操作性,其中 __array__ 是最古老且最不理想的。

注意

如果具有 __array__ 方法的类(无论是 ndarray 的子类还是非子类)用作 ufunc 的输出对象,结果将不会写入 __array__ 返回的对象。此做法将导致 TypeError

矩阵对象#

注意

强烈建议不要使用 matrix 子类。如下所述,它使得编写能够一致处理矩阵和常规数组的函数非常困难。目前,它们主要用于与 scipy.sparse 交互。我们希望为此用途提供替代方案,并最终移除 matrix 子类。

matrix 对象继承自 ndarray,因此它们拥有 ndarrays 的相同属性和方法。然而,matrix 对象有六个重要的区别,当您使用 matrices 时,这些区别可能会导致与预期数组行为不符的意外结果。

  1. Matrix 对象可以使用字符串表示法创建,以允许类似 Matlab 的语法,其中空格分隔列,分号(‘;’)分隔行。

  2. Matrix 对象始终是二维的。这具有深远的影响,因为 m.ravel() 仍然是二维的(第一维为 1),并且项选择会返回二维对象,因此序列行为与数组根本不同。

  3. Matrix 对象重载了乘法运算,使其成为矩阵乘法。请确保您了解这一点,特别是对于可能接收 matrix 的函数。尤其考虑到 asanyarray(m) 在 m 是 matrix 时会返回一个 matrix。

  4. Matrix 对象重载了幂运算,使其成为矩阵的幂。在使用 asanyarray(…) 获取 array 对象的函数内部使用幂运算时,存在相同的警告。

  5. Matrix 对象的默认 __array_priority__ 为 10.0,因此与 ndarrays 的混合运算始终会产生 matrices。

  6. Matrices 具有一些特殊的属性,可以简化计算。它们是:

    matrix.T

    返回矩阵的转置。

    matrix.H

    返回 self 的(复共轭)转置。

    matrix.I

    返回可逆 self 的(乘法)逆。

    matrix.A

    self 返回为 ndarray 对象。

警告

Matrix 对象重载了乘法 '*' 和幂运算 '**',分别用于矩阵乘法和矩阵幂。如果您的子程序可以接受子类并且您不将其转换为基类数组,则必须使用 ufuncs multiply 和 power 来确保您对所有输入执行正确的操作。

matrix 类是 ndarray 的一个 Python 子类,并且可以作为构造自己的 ndarray 子类的参考。Matrices 可以从其他 matrices、字符串以及任何可以转换为 ndarray 的内容创建。在 NumPy 中,“mat”是“matrix”的别名。

matrix(data[, dtype, copy])

从类数组对象或数据字符串返回一个矩阵。

asmatrix(data[, dtype])

将输入解释为矩阵。

bmat(obj[, ldict, gdict])

从字符串、嵌套序列或数组构建一个矩阵对象。

示例 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(),它必须由用户手动调用,以确保对数组的任何更改实际上都已写入磁盘。

memmap(filename[, dtype, mode, offset, ...])

创建对存储在磁盘上的 *二进制* 文件中的数组的内存映射。

memmap.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 开始,如果需要字符串数组,推荐使用 dtypeobject_bytes_str_ 的数组,并使用 numpy.char 模块中的免费函数进行快速矢量化字符串操作。

这些是增强型的 str_ 类型或 bytes_ 类型数组。这些数组继承自 ndarray,但专门定义了(广播的)逐元素操作 +*%。这些操作在标准字符类型的 ndarray 上不可用。此外,chararray 具有所有标准的 str(和 bytes)方法,并逐个元素地执行它们。也许创建 chararray 的最简单方法是使用 self.view(chararray),其中 *self* 是一个 str 或 unicode 数据类型的 ndarray。然而,也可以使用 chararray 构造函数或通过 numpy.char.array 函数创建 chararray。

char.chararray(shape[, itemsize, unicode, ...])

提供字符串和 unicode 值数组的便捷视图。

char.array(obj[, itemsize, copy, unicode, order])

创建一个 chararray

与 str 数据类型的标准 ndarray 的另一个区别是,chararray 继承了 Numarray 引入的功能,即在数组的任何元素的末尾的空白在项检索和比较操作时将被忽略。

记录数组#

NumPy 提供了 recarray 类,该类允许将结构化数组的字段作为属性进行访问,以及一个相应的标量数据类型对象 record

recarray(shape[, dtype, buf, offset, ...])

构造一个允许通过属性进行字段访问的 ndarray。

record(length_or_data, /[, dtype])

一个数据类型标量,允许将字段访问作为属性查找。

注意

pandas DataFrame 比 record array 功能更强大。如果可能,请使用 pandas DataFrame。

掩码数组(numpy.ma#

另请参阅

掩码数组

标准容器类#

为了向后兼容并作为标准的“容器”类,Numeric 的 UserArray 已被引入 NumPy 并命名为 numpy.lib.user_array.container。container 类是一个 Python 类,其 self.array 属性是 ndarray。与 ndarray 本身相比,使用 numpy.lib.user_array.container 进行多重继承可能更容易,因此它默认包含在内。这里对其进行记录仅仅是提及它的存在,因为如果您可以,鼓励您直接使用 ndarray 类。

numpy.lib.user_array.container(data[, ...])

用于轻松多重继承的标准容器类。

数组迭代器#

迭代器是数组处理的强大概念。本质上,迭代器实现了通用的 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]]

扁平迭代#

ndarray.flat

数组的 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 维枚举#

ndenumerate(arr)

多维索引迭代器。

有时在迭代时获取 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

广播迭代器#

broadcast(*arrays)

生成一个模拟广播的对象。

广播的通用概念也可以通过 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))