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 提供了 intfloatcomplex 等数值类型,以及字符串、日期时间(datetime)和结构化数据类型功能。然而,不断壮大的 Python 社区需要更多样化的数据类型。例如,附带单位信息(如米)的数据类型或分类数据类型(固定的一组可能值)。但当前的 NumPy 数据类型 API 过于受限,无法允许创建这些类型。

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

动机与范围#

另请参阅

用户影响部分包含了拟议更改从长远来看将启用的新型数据类型的示例。因此,可以不按顺序阅读这些部分。

动机#

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

支持参数化数据类型的这种附加功能增加了 NumPy 自身的复杂性,而且外部用户定义的数据类型也无法使用。一般来说,不同数据类型关注点封装性不佳。内部 C 结构的暴露加剧了这一负担,限制了新字段的添加(例如支持新的排序方法 [new_sort])。

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

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

  • 类型提升,例如决定浮点数和整数相加应返回浮点数的运算,对于数值数据类型非常有价值,但对于用户定义数据类型,尤其是参数化数据类型,其适用范围有限。

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

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

解决这些问题的巨大需求推动了科学界在多个项目中创建变通方案,将物理单位实现为类似数组的类而非数据类型,这将更好地推广到多个类似数组的库(如 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 及其后续涉及的 NumPy 数据类型定义方式的大规模重构预计会产生一些小的不兼容性(请参阅向后兼容性部分)。然而,预计不会出现需要大量代码修改的过渡,也不在本范围之内。

具体来说,本 NEP 作出以下设计选择,这些选择将在详细描述部分进行更详细的讨论:

  1. 每个数据类型都将是 np.dtype 子类的一个实例,其中大部分数据类型特定的逻辑将作为类的特殊方法实现。在 C-API 中,这些对应于特定的槽位。简而言之,对于 f = np.dtype("f8")isinstance(f, np.dtype) 仍然为真,但 type(f) 将是 np.dtype 的子类,而不仅仅是 np.dtype 本身。目前作为指针存储在实例上(作为 PyArray_Descr->f)的 PyArray_ArrFuncs,应改为像 Python 中通常那样存储在类上。将来,这些可能对应于 Python 端的双下划线方法。存储信息,如 `itemsize` 和 `byteorder`,可以在不同的 dtype 实例之间有所不同(例如“S3”与“S8”),并将保留为实例的一部分。这意味着从长远来看,当前对 dtype 方法的低级访问将被移除(参阅 NEP 40 中的 PyArray_ArrFuncs)。

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

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

此外,公共 API 将以一种未来可扩展的方式进行设计:

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

数据类型系统可能会被设计为与 NumPy 数组协同工作,例如通过提供步进循环,但应避免与数组对象(通常是 np.ndarray 实例)直接交互。相反,设计原则是数组对象是数据类型的消费者。虽然这只是一个指导原则,但这可能允许将数据类型系统甚至 NumPy 数据类型拆分为 NumPy 依赖的独立项目。

第二阶段数据类型系统的更改必须包括对 UFunc 机制的大规模重构,这将在 NEP 43 中进一步定义:

  1. 为了实现新用户定义数据类型的所有所需功能,UFunc 机制将进行更改,以取代当前的调度和类型解析系统。旧系统应在一段时间内*大部分*作为遗留版本得到支持。

此外,作为一项通用设计原则,添加新的用户定义数据类型将*不会*改变程序的行为。例如,common_dtype(a, b) 不能是 c,除非 ab 知道 c 存在。

用户影响#

当前生态系统中很少有使用 NumPy 的用户定义数据类型,其中最突出的是:rationalquaternion。这些代表了相当简单的数据类型,不受当前限制的强烈影响。然而,我们已经识别出对以下数据类型的需求:

  • bfloat16,用于深度学习

  • 分类类型

  • 物理单位(如米)

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

  • 高精度、固定精度数学

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

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

  • 扩展例如整数 dtypes 以拥有一个哨兵 NA 值

  • 几何对象 [pygeos]

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

实现本 NEP 的长期用户影响将是,通过引入这些新数据类型来促进整个生态系统的发展,并在 NumPy 内部整合这些数据类型的实现,以实现更好的互操作性。

示例#

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

简单数值类型#

主要用于内存受限的场景,诸如 bfloat16 等低精度数值类型在其他计算框架中很常见。对于这些类型,np.common_typenp.can_cast 等的定义是最重要的接口之一。一旦它们支持 np.common_type,(在大多数情况下)就有可能找到要调用的正确 ufunc 循环,因为大多数 ufuncs(例如加法)实际上只需要 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 具有比 dps=4mpf 数据类型更高的精度。

或者,我们可以说

>>> 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") 是一个带有米单位的 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 输出数据类型确定比简单的数值 dtype 更复杂,因为没有“通用”输出类型。

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

事实上,np.result_type(meters, seconds) 在没有操作上下文的情况下必须报错。这个例子强调了特定的 ufunc 循环(以已知、特定的 DType 作为输入的循环)必须在实际计算开始之前做出某些决定。

实现#

全面重构方案#

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

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

    • 将数据类型组织成正常的 Python 类 [PR 15508]_

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

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

      • 类层次结构和 DType 类本身的属性,包括未被以下最核心点涵盖的方法。

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

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

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

    • 重构通用函数的工作方式(NEP 43),以允许扩展 ~numpy.ufunc,例如 np.add,使其适用于用户定义的数据类型(如单位)

      • 重构低级 C 函数的组织方式,使其足够可扩展和灵活,以适应像单位这样复杂的 DType。

      • 实现用户定义的这些低级 C 函数的注册和高效查找。

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

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

    • 清理被认为是错误或不合时宜的遗留行为。

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

    • 协助社区创建单位或分类等类型。

    • 允许字符串用于 np.equalnp.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 *内部*也未使用)将不再受支持。

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

  • dtype 实现者 (C-API):

    • 目前提供给某些函数(如类型转换函数)的数组将不再提供。例如,PyArray_Descr->f->nonzeroPyArray_Descr->f->copyswapn,可能会改为接收一个仅包含部分字段(主要是 dtype)的虚拟数组对象,这些字段是有效的。至少在某些代码路径中,已经使用了类似的机制。

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

    • 目前,用户 dtypes 被定义为 np.dtype 的实例。创建时用户提供一个原型实例。NumPy 在注册期间至少需要修改类型。这对 rationalquaternion 都没有影响,并且在注册后结构发生变异的可能性不大。

由于数据类型相关的 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.kinddtype.chardtype.type 进行此检查。

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

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

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

从计算机科学的角度来看,类型定义了*值空间*(其实例可以取的所有可能值)及其*行为*。如本 NEP 中所提议,DType 类定义了值空间和行为。dtype 实例可以被视为值的一部分,因此典型的 Python instance 对应于 dtype + element(其中 *element* 是存储在数组中的数据)。另一种观点是直接在 dtype 实例上定义值空间和行为。以下图表展示了这两种选项,并与类似的 Python 实现模式进行了比较。

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

区别在于如何处理参数(例如字符串长度或日期时间单位(msns 等))和存储选项(例如字节序)。在实现 Python(标量)type 时,例如日期时间单位等参数将存储在实例中。这是 NEP 42 试图模仿的设计,然而,参数现在是 dtype 实例的一部分,这意味着实例中存储的部分数据由所有数组元素共享。如前所述,这意味着 Python instance 对应于存储在 NumPy 数组中的 dtype + element

Python 中更高级的方法是使用类工厂和抽象基类 (ABC)。这允许将参数移动到动态创建的 type 中,并且行为实现可能针对这些参数。另一种方法可能会使用此模型,并直接在 dtype 实例上实现行为。

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

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

对于简单的如 float64(另请参阅下文)的数据类型,np.dtype("float64") 的实例可以直接作为标量似乎很有吸引力。由于标量而非数据类型目前定义了一个有用的类型层次结构,这一想法可能更具吸引力。

然而,我们出于多种原因明确反对这一点。首先,本文所述的新数据类型将是 DType 类的实例。将这些实例本身也设为类,虽然可能,但会增加用户需要理解的额外复杂性。这也意味着标量必须包含存储信息(如字节序),这通常是不必要的,并且目前也未使用。其次,尽管像 float64 这样的简单 NumPy 标量可能是此类实例,但应该可以在不强制 NumPy 作为依赖项的情况下为 Python 对象创建数据类型。然而,不依赖 NumPy 的 Python 对象不能是 NumPy DType 的实例。第三,对标量和数据类型有用的方法和属性之间存在不匹配。例如,to_float() 对于标量有意义,但对于数据类型则不然,而 newbyteorder 对标量没有用处(或具有不同的含义)。

总的来说,将标量作为 DType 的实例似乎不仅不能降低复杂性(即通过合并两个不同的类型层次结构),反而会增加设计和实现的复杂性。

未来可能的路径是简化当前的 NumPy 标量,使其成为更简单的对象,其行为主要源自数据类型。

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

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

因此,新版本将取代当前用于定义新数据类型的 PyArray_ArrFuncs 结构。目前存在并使用这些槽位定义的数据类型将在弃用期内得到支持。

最可能的解决方案是从用户那里隐藏实现细节,从而使其在未来可扩展,即参照 Python 的稳定 API [PEP-384] 设计 API。

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 侧的方法,例如 dtype.__dtype_method__,尽管向 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。