NEP 41 — 新数据类型系统的第一步#

标题:

新数据类型系统的第一步

作者:

Sebastian Berg

作者:

Stéfan van der Walt

作者:

Matti Picus

状态:

已接受

类型:

标准轨道

创建时间:

2020-02-03

解决时间:

https://mail.python.org/pipermail/numpy-discussion/2020-April/080573.htmlhttps://mail.python.org/pipermail/numpy-discussion/2020-March/080495.html

注意

此 NEP 是系列中的第二篇

  • NEP 40 解释了NumPy的dtype实现的不足。

  • NEP 41(本文档)概述了我们提议的替换方案。

  • NEP 42 描述了新设计中与数据类型相关的API。

  • NEP 43 描述了新设计的通用函数 API。

摘要#

NumPy 中的数据类型描述了如何解释数组中的每个元素。NumPy 提供了 `int`、`float` 和 `complex` 数值类型,以及字符串、日期时间(datetime)和结构化数据类型功能。然而,日益壮大的 Python 社区需要更多样化的数据类型。例如,带有单位信息(如米)的数据类型,或类别型数据类型(固定的一组可能值)。但是,当前的 NumPy 数据类型 API 太有限,无法创建这些。

此 NEP 是实现此类增长的第一步;它将为新数据类型的开发带来更简单的路径。长远来看,新数据类型系统还将支持直接从 Python 而非 C 创建数据类型。重构数据类型 API 将提高可维护性,并促进 NumPy 内部现有数据类型的新功能以及用户自定义外部数据类型的发展。

动机与范围#

另请参阅

用户影响部分包含了一些示例,说明了长远来看所提出的更改将能够实现哪些类型的新数据类型。因此,可能有助于按非顺序阅读这些部分。

动机#

当前 API 的主要问题之一是参数化数据类型(另请参见NEP 40)的典型函数(如加法和乘法)的定义,这需要额外的步骤来确定输出类型。例如,当两个长度为 4 的字符串相加时,结果是一个长度为 8 的字符串,这与输入不同。同样,嵌入物理单位的数据类型必须计算新的单位信息:将距离除以时间会得到速度。一个相关的难题是,当前的转换规则——不同数据类型之间的转换——无法描述 NumPy 外部实现的此类参数化数据类型的转换。

支持参数化数据类型的这些附加功能增加了 NumPy 内部的复杂性,并且此外,外部用户定义的数据类型也无法使用。总的来说,不同数据类型的关注点没有得到很好的封装。通过暴露内部 C 结构,这种负担进一步加剧,限制了新字段的添加(例如,以支持新的排序方法[new_sort])。

目前有许多因素限制了新用户定义数据类型的创建

  • 为参数化用户定义数据类型创建转换规则要么不可能,要么过于复杂,以至于从未尝试过。

  • 类型提升(type promotion),例如,操作决定将浮点数和整数值相加应返回浮点数值,这对数值数据类型非常有价值,但对于用户定义和特别是参数化数据类型的范围有限。

  • 许多逻辑(例如,提升)被写在单个函数中,而不是作为数据类型本身的成员函数进行拆分。

  • 在当前设计中,数据类型不能拥有不适用于其他数据类型的方法。例如,单位数据类型不能有一个 `.to_si()` 方法来轻松找到表示相同值的 SI 单位的数据类型。

解决这些问题的巨大需求促使科学界在多个项目中创建了变通方法,将物理单位实现为类数组(array-like)而不是数据类型,这在多个类数组(Dask、pandas 等)之间将具有更好的通用性。Pandas 已经通过其扩展数组[pandas_extension_arrays]向同一方向迈出了一大步,毫无疑问,如果这些新功能能够在 NumPy、Pandas 和其他项目之间通用,那么社区将受益最大。

范围#

拟议的数据类型系统重构是一项艰巨的任务,因此提议将其分为几个阶段,大致如下:

  • 第一阶段:重构和扩展数据类型基础设施(本 NEP 41)

  • 第二阶段:逐步定义或重构 API(主要在 NEP 42/43 中详细说明)

  • 第三阶段:NumPy 和科学 Python 生态系统功能的增长。

有关各个阶段的更详细说明,请参阅下面的“实现”部分中的“实施完整重构的计划”。此 NEP 提议继续进行必要的新 dtype 子类创建(第一阶段),并开始着手实现当前功能。在此 NEP 的上下文中,所有开发都将是完全私有的 API 或使用初步的下划线名称,这些名称将来必须更改。大多数内部和公共 API 选择属于第二阶段,将在随后的 NEP 42 和 43 中更详细地讨论。此 NEP 的初步实现对用户几乎没有影响,但为逐步解决完整重构提供了必要的基础。

此 NEP 和后续隐含的大规模数据类型定义重构预计将产生一些小的兼容性问题(参见向后兼容性部分)。然而,预计不会出现需要大量代码调整的过渡,并且这不属于范围。

具体而言,此 NEP 做出以下设计选择,这些选择将在详细描述部分中更详细地讨论:

  1. 每个数据类型都将是 `np.dtype` 子类的一个实例,大部分数据类型特定逻辑将作为类上的特殊方法实现。在 C-API 中,这些对应于特定的槽。简而言之,对于 `f = np.dtype("f8")`,`isinstance(f, np.dtype)` 将保持为真,但 `type(f)` 将是 `np.dtype` 的子类,而不是 `np.dtype` 本身。目前作为实例上的指针存储的 `PyArray_ArrFuncs`(作为 `PyArray_Descr->f`),应该像通常在 Python 中那样存储在类上。将来,这些可能对应于 Python 端的 dunder 方法。诸如 itemsize 和 byteorder 等存储信息可能因不同的 dtype 实例而异(例如,“S3” vs. “S8”),并且将继续作为实例的一部分。这意味着长远来看,当前的低级访问 dtype 方法将被移除(参见NEP 40 中的 `PyArray_ArrFuncs`)。

  2. 当前的 NumPy 标量(scalars)*不会*改变,它们不会是数据类型的实例。对于新数据类型,标量也不会是 dtype 的实例(尽管 `isinstance(scalar, dtype)` 可能会在适当的时候返回 `True`)。

详细的技术决策将在 NEP 42 中跟进。

此外,公共 API 的设计方式将在未来具有可扩展性。

  1. 提供给用户的所有新的 C-API 函数将尽可能隐藏实现细节。公共 API 应该是内部 NumPy 数据类型使用的 C-API 的相同但受限的版本。

数据类型系统可能以 NumPy 数组为目标,例如通过提供跨步循环(strided-loops),但应避免直接与数组对象(通常是 `np.ndarray` 实例)交互。相反,设计原则将是数组对象是数据类型的消费者。虽然这只是一个指导原则,但它可能允许将数据类型系统甚至 NumPy 数据类型拆分成自己的项目,NumPy 将依赖于该项目。

第二阶段数据类型系统的更改必须包括对通用函数(UFunc)机制的大规模重构,这将由 NEP 43 进一步定义。

  1. 为了支持新的用户定义数据类型的所有期望功能,通用函数(UFunc)机制将进行更改,以替换当前的调度和类型解析系统。旧系统应该在一段时间内*主要*支持为旧版本。

此外,作为一般设计原则,添加新的用户定义数据类型*不会*改变程序的行为。例如,`common_dtype(a, b)` 必须不等于 `c`,除非 `a` 或 `b` 知道 `c` 的存在。

用户影响#

当前生态系统中用户定义的 NumPy 数据类型非常少,最突出的两个是:`rational` 和 `quaternion`。它们代表相当简单的数据类型,不受当前限制的强烈影响。然而,我们已经确定了对以下数据类型的需求:

  • bfloat16,用于深度学习

  • 类别类型

  • 物理单位(如米)

  • 用于追踪/自动微分的数据类型

  • 高精度固定点数学

  • 专用整数类型,如 int2、int24

  • 新的、更好的日期时间表示

  • 扩展例如整数 dtype 以具有 sentinel NA 值

  • 几何对象 [pygeos]

其中一些问题已部分解决;例如,`astropy.units`、`unyt` 或 `pint` 中提供了单位功能,作为 `numpy.ndarray` 的子类。然而,大多数这些数据类型目前根本无法合理定义。在 NumPy 中拥有这些数据类型的优势在于,它们应该能与其他数组或类数组包(如 Pandas、`xarray` [xarray_dtype_issue] 或 `Dask`)无缝集成。

实施此 NEP 的长期用户影响将是通过这些新数据类型来促进整个生态系统的增长,并巩固 NumPy 内部这些数据类型的实现以实现更好的互操作性。

示例#

以下示例代表了我们希望实现的未来用户定义数据类型。这些数据类型不属于 NEP 的一部分,其选择(例如,转换规则的选择)是我们希望实现的可能性,并不代表推荐。

简单的数值类型#

主要在内存是考虑因素的地方使用,较低精度的数值类型,如 bfloat16 在其他计算框架中很常见。对于这些类型,`np.common_type` 和 `np.can_cast` 等内容的定义是最重要的接口。一旦它们支持 `np.common_type`,就可以(在大多数情况下)找到正确的 ufunc 循环来调用,因为大多数 ufunc——如 add——实际上只需要 `np.result_type`。

>>> np.add(arr1, arr2).dtype == np.result_type(arr1, arr2)

而 `~numpy.result_type` 在很大程度上等同于 `~numpy.common_type`。

固定、高精度数学#

允许任意精度或更高精度的数学在模拟中很重要。例如,`mpmath` 定义了一个精度。

>>> import mpmath as mp
>>> print(mp.dps)  # the current (default) precision
15

NumPy 应该能够从 `mpmath.mpf` 浮点对象列表构建一个本地、内存高效的数组。

>>> arr_15_dps = np.array(mp.arange(3))  # (mp.arange returns a list)
>>> print(arr_15_dps)  # Must find the correct precision from the objects:
array(['0.0', '1.0', '2.0'], dtype=mpf[dps=15])

我们还应该能够在创建数组的数据类型时指定所需的精度。在这里,我们使用 `np.dtype[mp.mpf]` 来查找 DType 类(此表示法不属于此 NEP),然后用所需的参数进行实例化。这也可以写成 `MpfDType` 类。

>>> arr_100_dps = np.array([1, 2, 3], dtype=np.dtype[mp.mpf](dps=100))
>>> print(arr_15_dps + arr_100_dps)
array(['0.0', '2.0', '4.0'], dtype=mpf[dps=100])

`mpf` 数据类型可以决定操作的结果应该是两者中较高的精度,因此使用 100 的精度。此外,我们应该能够定义转换,例如在

>>> np.can_cast(arr_15_dps.dtype, arr_100_dps.dtype, casting="safe")
True
>>> np.can_cast(arr_100_dps.dtype, arr_15_dps.dtype, casting="safe")
False  # loses precision
>>> np.can_cast(arr_100_dps.dtype, arr_100_dps.dtype, casting="same_kind")
True

从浮点数转换通常至少是 `same_kind` 转换,但总的来说,它不安全。

>>> np.can_cast(np.float64, np.dtype[mp.mpf](dps=4), casting="safe")
False

因为 float64 的精度高于 `mpf` 数据类型(精度为 `dps=4`)。

或者,我们可以说:

>>> np.common_type(np.dtype[mp.mpf](dps=5), np.dtype[mp.mpf](dps=10))
np.dtype[mp.mpf](dps=10)

甚至可能

>>> np.common_type(np.dtype[mp.mpf](dps=5), np.float64)
np.dtype[mp.mpf](dps=16)  # equivalent precision to float64 (I believe)

因为 `np.float64` 可以安全地转换为 `np.dtype[mp.mpf](dps=16)`。

类别型#

类别型之所以有趣,是因为它们可以具有固定的、预定义的值,或者可以是动态的,能够在必要时修改类别。固定类别(预先定义)是最直接的类别定义。类别型是*困难的*,因为有许多实现它们的策略,这表明 NumPy 应该只提供用户定义类别类型的脚手架。例如:

>>> cat = Categorical(["eggs", "spam", "toast"])
>>> breakfast = array(["eggs", "spam", "eggs", "toast"], dtype=cat)

因为它只知道有 3 个类别,所以可以非常高效地存储数组。由于这种意义上的类别型几乎不知道它存储的数据,因此很少有操作有意义,尽管相等性有意义。

>>> breakfast2 = array(["eggs", "eggs", "eggs", "eggs"], dtype=cat)
>>> breakfast == breakfast2
array[True, False, True, False])

类别型数据类型可以像字典一样工作:没有两个项目名称可以相等(在 dtype 创建时检查),因此上面的相等操作可以非常高效地执行。如果值定义了顺序,则类别标签(内部为整数)可以按相同方式排序,以允许高效排序和比较。

从具有较少值的类别转换为具有严格更多值的类别是否定义了转换,这是类别型数据类型需要决定的。两种选项都应该可用。

数据类型上的单位#

定义单位有不同的方法,取决于内部机制的组织方式,一种方法是为每种现有的数值类型提供一个单一的单位数据类型。这将写成 `Unit[float64]`,单位本身是 DType 实例的一部分 `Unit[float64]("m")` 是一个带有米(meters)附加的 `float64`。

>>> from astropy import units
>>> meters = np.array([1, 2, 3], dtype=np.float64) * units.m  # meters
>>> print(meters)
array([1.0, 2.0, 3.0], dtype=Unit[float64]("m"))

请注意,单位有点棘手。是否

>>> np.array([1.0, 2.0, 3.0], dtype=Unit[float64]("m"))

(将没有单位的浮点数标量强制转换为米)应为有效语法是有争议的。一旦创建了数组,数学运算就不会有问题。

>>> meters / (2 * unit.seconds)
array([0.5, 1.0, 1.5], dtype=Unit[float64]("m/s"))

从一个单位到另一个单位的转换无效,但可以在不同尺度的相同维度之间有效(尽管这可能是“不安全的”)。

>>> meters.astype(Unit[float64]("s"))
TypeError: Cannot cast meters to seconds.
>>> meters.astype(Unit[float64]("km"))
>>> # Convert to centimeter-gram-second (cgs) units:
>>> meters.astype(meters.dtype.to_cgs())

上述表示法有些笨拙。可以使用函数来转换单位。可能有方法使它们更方便,但这些应留待将来讨论。

>>> units.convert(meters, "km")
>>> units.to_cgs(meters)

存在一些未解决的问题。例如,数组对象上是否存在其他方法来简化某些概念,以及这些概念将如何从数据类型传递到 `ndarray`。

与其他标量的交互很可能通过以下方式定义:

>>> np.common_type(np.float64, Unit)
Unit[np.float64](dimensionless)

通用函数(UFunc)的输出数据类型确定可能比简单的数值数据类型更复杂,因为没有“通用”输出类型。

>>> np.multiply(meters, seconds).dtype != np.result_type(meters, seconds)

事实上,`np.result_type(meters, seconds)` 在没有操作上下文的情况下必须出错。这个例子突出了特定的通用函数循环(具有已知、特定 DTypes 作为输入的循环)在实际计算开始之前必须能够做出某些决定。

实现#

实施完整重构的计划#

为了解决 NumPy 中的这些问题并启用新数据类型,需要多个开发阶段:

  • 第一阶段:重构和扩展数据类型基础设施(本 NEP)

    • 像正常的 Python 类一样组织数据类型 [PR 15508]_

  • 第二阶段:逐步定义或重构 API

    • 通过 DType 上的方法和属性逐步定义所有必要功能(NEP 42)。

      • 类层次结构和 DType 类本身的属性,包括以下最核心的几点之外的方法。

      • 支持使用 `arr.astype()` 进行 dtype 转换以及转换相关操作(如 `np.common_type`)的功能。

      • 项目访问和存储的实现,以及使用 `np.array()` 创建数组时形状和 dtype 的确定方式。

      • 创建公共 C-API 以定义新的 DType。

    • 重构通用函数(UFunc)的工作方式(NEP 43),以允许为用户定义的数据类型(如 Units)扩展 `~numpy.ufunc`(如 `np.add`)。

      • 重构低级 C 函数的组织方式,使其具有足够的扩展性和灵活性以处理复杂的 DType(如 Units)。

      • 定义如何使用用户定义的低级 C 函数的注册和高效查找。

      • 定义如何使用类型提升来实现需要转换时的行为。例如,`np.float64(3) + np.int32(3)` 将 `int32` 提升为 `float64`。

  • 第三阶段:NumPy 和科学 Python 生态系统功能的增长。

    • 清理被认为有错误或不希望的旧行为。

    • 提供从 Python 定义新数据类型的途径。

    • 协助社区创建 Units 或 Categoricals 等类型。

    • 允许字符串在 `np.equal` 或 `np.add` 等函数中使用。

    • 移除 NumPy 内部的旧代码路径以提高长期可维护性。

本文档作为第一阶段的基础,并为整个项目提供了愿景和动机。第一阶段不引入任何新的面向用户的特性,而是关注当前数据类型系统的必要概念性清理。它提供了一个更“pythonic”的数据类型 Python 类型对象,具有清晰的类层次结构。

第二阶段是逐步创建定义功能齐全数据类型所需的所有 API,以及重新组织 NumPy 数据类型系统。因此,此阶段将主要关注定义一个(最初是初步的)稳定的公共 API。

大规模重构的一些好处可能只有在当前旧实现完全弃用后(即更大的代码移除)才能显现。然而,这些步骤对于改进 NumPy 核心 API 的许多部分至关重要,并有望使实现更易于理解。

下图高层次地说明了提议的设计,并大致划分了整体设计的组成部分。请注意,此 NEP 仅涉及第一阶段(阴影区域),其余部分包含第二阶段,设计选择尚待讨论,但它强调了 DType 数据类型类是核心的、必需的概念。

_images/nep-0041-mindmap.svg

向后兼容性#

虽然实施第一阶段和第二阶段的实际向后兼容性影响尚不完全清楚,但我们预计并接受以下更改:

  • Python API:

    • `type(np.dtype("f8"))` 将是 `np.dtype` 的子类,而现在 `type(np.dtype("f8")) is np.dtype`。代码应该使用 `isinstance` 检查,在极少数情况下可能需要适配使用它。

  • C-API:

    • 在旧版本的 NumPy 中,`PyArray_DescrCheck` 是一个使用 `type(dtype) is np.dtype` 的宏。当编译针对旧的 NumPy 版本时,该宏可能需要替换为相应的 `PyObject_IsInstance` 调用。(如果这是一个问题,我们可以回溯修复宏)。

    • 通用函数(UFunc)机制的更改将破坏当前实现的*有限*部分。替换例如默认的 `TypeResolver` 预计将支持一段时间,尽管优化的掩码内部循环迭代(甚至在 NumPy 内部都不使用)将不再受支持。

    • 目前在数据类型上定义的所有函数,例如 `PyArray_Descr->f->nonzero`,将被以不同的方式定义和访问。这意味着从长远来看,低级访问代码将不得不进行更改以使用新的 API。这类更改预计只需要在非常少的项目中。

  • dtype 实现者(C-API):

    • 目前提供给某些函数(如转换函数)的数组将不再提供。例如,`PyArray_Descr->f->nonzero` 或 `PyArray_Descr->f->copyswapn`,可能会收到一个只有某些字段(主要是 dtype)有效的虚拟数组对象。至少在某些代码路径中,已经使用了类似的机制。

    • `scalarkind` 槽和标量转换的注册将被移除/忽略,没有替代。它目前允许基于部分值的转换。`PyArray_ScalarKind` 函数将继续适用于内置类型,但不会在内部使用,并将被弃用。

    • 目前用户 dtype 被定义为 `np.dtype` 的实例。创建工作是通过用户提供一个原型实例。NumPy 需要至少在注册期间修改类型。这对 `rational` 或 `quaternion` 都没有影响,并且注册后结构的突变不太可能发生。

由于数据类型相关的 API 表面相当大,可能会进一步更改或将某些函数限制为现有数据类型。例如,使用类型号作为输入的函数应替换为接受 DType 类的函数。尽管是公共的,但此 C-API 的大部分似乎很少,可能从未使用过下游项目。

详细描述#

本节详细介绍了此 NEP 所涵盖的设计决策。子部分对应于“范围”部分中呈现的设计选择列表。

数据类型作为 Python 类(1)#

当前的 NumPy 数据类型不是完整的 Python 类。它们而是单个 `np.dtype` 类的(原型)实例。改变这一点意味着任何特殊处理,例如 `datetime`,都可以移到 Datetime DType 类中,而不是笨重的通用代码(例如,当前的 `PyArray_AdjustFlexibleDType`)。

与 API 相关的此更改的主要后果是,特殊方法将从 dtype 实例移动到新的 DType 类的方法。这是 Python 中使用的典型设计模式。以更 Pythonic 的方式组织这些方法和信息,为将来精炼和扩展 API 提供了坚实的基础。由于其公开的方式,当前的 API 无法扩展。这意味着,例如,目前存储在每个数据类型上的 `PyArray_ArrFuncs` 中的方法(参见NEP 40)将来将被不同地定义,并且从长远来看将被弃用。

这将最显著可见的副作用是 `type(np.dtype(np.float64))` 将不再是 `np.dtype`。相反,它将是 `np.dtype` 的子类,这意味着 `isinstance(np.dtype(np.float64), np.dtype)` 将保持为真。这还将添加使用 `isinstance(dtype, np.dtype[float64])` 的能力,从而消除了使用 `dtype.kind`、`dtype.char` 或 `dtype.type` 来进行此检查的需要。

通过将 DTypes 设计为完整的 Python 类,出现了子类化的问题。然而,继承似乎有问题,并且是一个最好(至少初步)避免的复杂性,特别是对于容器数据类型。此外,子类可能对于互操作性更有趣,例如与 GPU 后端(CuPy)存储与 GPU 相关的额外方法,而不是作为定义新数据类型的机制。类层次结构确实有价值,并且可以通过允许创建*抽象*数据类型来实现。抽象数据类型的示例是 `np.floating` 的数据类型等价物,它表示任何浮点数。这些可以起到与 Python 的抽象基类(abstract base classes)相同的目的。

此 NEP 选择完全或部分复制标量层次结构。主要原因是解耦 DType 和标量的实现。要将 DType 添加到 NumPy,理论上标量不需要修改或了解 NumPy。另请注意,Pandas 中目前实现的类别 DType 没有标量对应项,这使得依赖标量来实现行为的难度更大。虽然 DType 和 Scalar 描述的是相同的概念/类型(例如 `int64`),但将 NumPy 所需的信息和功能分离到 DType 类中似乎是实用的。

dtype 实例提供参数和存储选项#

从计算机科学的角度来看,类型定义了其实例可以具有的*值空间*(所有可能值)以及它们的*行为*。正如本 NEP 所提议的,DType 类定义了值空间和行为。<code class="docutils literal notranslate"><span class="pre">dtype</span></code> 实例可以被视为值的一部分,因此典型的 Python <code class="docutils literal notranslate"><span class="pre">instance</span></code> 对应于 <code class="docutils literal notranslate"><span class="pre">dtype</span> <span class="pre">+</span> <span class="pre">element</span></code>(其中 *element* 是存储在数组中的数据)。另一种观点是将值空间和行为直接定义在 <code class="docutils literal notranslate"><span class="pre">dtype</span></code> 实例上。这两种选择在下图呈现,并与类似的 Python 实现模式进行了比较。

_images/nep-0041-type-sketch-no-fonts.svg

区别在于参数(如字符串长度或日期时间单位(<code class="docutils literal notranslate"><span class="pre">ms</span></code><code class="docutils literal notranslate"><span class="pre">ns</span></code> 等)以及存储选项(如字节序)的处理方式。在实现 Python(标量)<code class="docutils literal notranslate"><span class="pre">type</span></code> 参数时,例如日期时间单位,将被存储在实例中。这是 NEP 42 试图模仿的设计,但是,参数现在是 dtype 实例的一部分,这意味着存储在实例中的部分数据被所有数组元素共享。如前所述,这意味着 Python <code class="docutils literal notranslate"><span class="pre">instance</span></code> 对应于存储在 NumPy 数组中的 <code class="docutils literal notranslate"><span class="pre">dtype</span> <span class="pre">+</span> <span class="pre">element</span></code>

Python 中更高级的方法是使用类工厂和抽象基类 (ABC)。这允许将参数移到动态创建的 <code class="docutils literal notranslate"><span class="pre">type</span></code> 中,并且行为实现可能特定于这些参数。另一种方法可能会使用此模型,并直接在 <code class="docutils literal notranslate"><span class="pre">dtype</span></code> 实例上实现行为。

我们相信此处提出的版本更容易使用和理解。Python 类工厂并不常用,NumPy 也不使用专门针对 dtype 参数或字节序的代码。使此类专用化更容易实现似乎不是一个优先事项。此选择的一个结果是,某些 DTypes 如果没有参数或存储变体,可能只有一个单例实例。但是,所有 NumPy dtypes 都需要动态创建的实例,因为允许附加元数据。

标量不应成为数据类型的实例(2)#

对于简单的例如 <code class="docutils literal notranslate"><span class="pre">float64</span></code> 的数据类型(也见下文),<code class="docutils literal notranslate"><span class="pre">np.dtype("float64")</span></code> 的实例似乎可以成为标量。由于标量而不是数据类型当前定义了一个有用的类型层次结构,因此这个想法可能更具吸引力。

然而,我们因多种原因特意反对这一点。首先,本文所述的新数据类型将是 DType 类的实例。使这些实例本身成为类,虽然可行,但增加了用户需要理解的额外复杂性。这也意味着标量必须具有存储信息(例如字节序),这通常是不必要的,并且目前未使用。其次,虽然简单的 NumPy 标量(如 <code class="docutils literal notranslate"><span class="pre">float64</span></code>)可以是此类实例,但应该能够为不强制要求 NumPy 作为依赖项的 Python 对象创建数据类型。然而,不依赖于 NumPy 的 Python 对象不能是 NumPy DType 的实例。第三,标量和数据类型有用的方法和属性之间存在不匹配。例如,<code class="docutils literal notranslate"><span class="pre">to_float()</span></code> 对标量有意义,但对数据类型没有意义,而 <code class="docutils literal notranslate"><span class="pre">newbyteorder</span></code> 对标量没有用(或者有不同的含义)。

总的来说,与其说减少了复杂性(例如通过合并两个不同的类型层次结构),不如说让标量成为 DTypes 的实例会增加设计和实现的复杂性。

未来可能的路径是简化当前的 NumPy 标量,使其成为更简单的对象,这些对象很大程度上从数据类型中派生其行为。

用于创建新数据类型的 C-API(3)#

用户可以创建新数据类型的当前 C-API 范围有限,并且需要使用“私有”结构。这意味着 API 不可扩展:在不丢失二进制兼容性的情况下,无法向结构添加新成员。这已经限制了将新的排序方法包含到 NumPy 中 [new_sort]

因此,新版本将取代用于定义新数据类型的当前 <code class="docutils literal notranslate"><span class="pre">PyArray_ArrFuncs</span></code> 结构。使用这些槽定义的数据类型将在弃用期内得到支持。

最可能的解决方案是将实现隐藏起来,从而使其在未来具有可扩展性,即模仿 Python 的稳定 API [PEP-384]

static struct PyArrayMethodDef slots[] = {
    {NPY_dt_method, method_implementation},
    ...,
    {0, NULL}
}

typedef struct{
  PyTypeObject *typeobj;  /* type of python scalar */
  ...;
  PyType_Slot *slots;
} PyArrayDTypeMeta_Spec;

PyObject* PyArray_InitDTypeMetaFromSpec(
        PyArray_DTypeMeta *user_dtype, PyArrayDTypeMeta_Spec *dtype_spec);

C 端槽应设计为镜像 Python 端的方法,例如 <code class="docutils literal notranslate"><span class="pre">dtype.__dtype_method__</span></code>,尽管向 Python 公开是实现过程的后续步骤,以降低初始实现的复杂性。

UFunc 机械装置的 C-API 更改(4)#

对 UFunc 机械装置的拟议更改将是 NEP 43 的一部分。但是,将需要以下更改(请参阅 NEP 40 以详细了解当前实现及其问题)。

  • 必须调整当前的 UFunc 类型解析,以便更好地控制用户定义的 dtypes 并解决当前的不一致之处。

  • UFunc 中使用的内循环必须扩展以包含返回值。此外,必须改进错误报告,并启用传递 dtype 特定的信息。这需要修改内循环函数签名,并添加在使用内循环之前和之后调用的新挂钩。

对通用函数进行任何更改的一个重要目标是允许重用现有循环。新的单位数据类型应该能够在其处理完单位相关的计算后,轻松地回退到现有的数学函数。

讨论#

有关以前的会议和讨论列表,请参阅 NEP 40

此特定 NEP 的其他讨论已在邮件列表和拉取请求上进行。

参考文献#

致谢#

创建 NumPy 新数据类型的努力已在许多不同环境和设置中讨论了数年,因此无法列出所有参与者。我们特别感谢 Stephan Hoyer、Nathaniel Smith 和 Eric Wieser 就数据类型设计进行的反复深入讨论。我们非常感谢社区在审查和修订本 NEP 方面的意见,并特别感谢 Ross Barnowski 和 Ralf Gommers。