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

标题:

迈向新数据类型系统的第一步

作者:

Sebastian Berg

作者:

Stéfan van der Walt

作者:

Matti Picus

状态:

已接受

类型:

标准轨道 (Standard Track)

创建时间:

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

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

动机与范围#

另请参阅

用户影响章节包含了提议的更改在长期内将启用哪些新数据类型的示例。因此,不按顺序阅读这些章节可能会有所帮助。

动机#

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

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

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

  • 为参数化用户定义 dtype 创建转换规则要么是不可能的,要么是极其复杂的,以至于从未有人尝试过。

  • 类型提升 (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 以及随后暗示的对 NumPy 中数据类型定义方式的大规模重做,预计会产生细微的不兼容性(参见向后兼容性部分)。但是,不预见且不计划需要大规模代码适配的过渡。

具体而言,本 NEP 做出以下设计选择,并在详细说明部分中详细讨论

  1. 每个数据类型都将是 np.dtype 子类的一个实例,大多数数据类型特定的逻辑将作为该类上的特殊方法来实现。在 C-API 中,这些对应于特定的插槽 (slots)。简而言之,对于 f = np.dtype("f8")isinstance(f, np.dtype) 将保持为真,但 type(f) 将是 np.dtype 的子类,而不仅仅是 np.dtype 本身。目前存储在实例指针上的 PyArray_ArrFuncs(作为 PyArray_Descr->f),应像 Python 中的典型做法一样存储在类上。未来,这些可能对应于 Python 端的双下划线方法。诸如 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 机制将被更改,以取代当前的调度和类型解析系统。旧系统应在一段时间内作为遗留版本提供*大部分*支持。

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

用户影响#

当前的生态系统中只有极少数使用 NumPy 的用户定义数据类型,最著名的两个是:rational (有理数) 和 quaternion (四元数)。这些代表了相当简单的数据类型,受当前局限性的影响不强。但是,我们已经确定了对以下数据类型的需求

  • bfloat16,用于深度学习

  • 分类 (categorical) 类型

  • 物理单位(如米)

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

  • 高精度、固定精度数学运算

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

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

  • 扩展例如整数 dtype 以拥有哨兵 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 循环,因为大多数 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

从 float 转换可能始终至少是 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)

分类类型 (Categoricals)#

分类类型的有趣之处在于它们可以具有固定的、预定义的值,也可以是动态的,并能够在必要时修改类别。固定类别(提前定义)是最直接的分类定义。分类类型很*难*,因为有很多策略可以实现它们,这表明 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) 数据类型。这将写为 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),以允许为用户定义的数据类型(如 Units)扩展 ~numpy.ufunc(如 np.add

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

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

      • 定义在需要转换时如何使用“提升” (promotion) 来实现行为。例如 np.float64(3) + np.int32(3)int32 提升为 float64

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

    • 清理被认为有缺陷或不受欢迎的遗留行为。

    • 提供一条从 Python 定义新数据类型的路径。

    • 协助社区创建诸如 Units 或 Categoricals 之类的类型

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

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

本文档作为第一阶段的基础,并为整个项目提供愿景和动力。第一阶段不引入任何面向用户的新功能,而是关注当前数据类型系统必要的概念清理。它提供了一个更符合 Python 习惯的 ("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 *内部*都未使用)将不再受支持。

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

  • dtype 实现者 (C-API):

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

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

    • 目前,用户 dtype 被定义为 np.dtype 的实例。其创建方式是用户提供一个原型实例。NumPy 将需要在注册期间至少修改其类型。这对于 rationalquaternion 都没有影响,而且注册后结构发生变异的可能性似乎很小。

由于存在相当大的关于数据类型的 API 表面,可能会发生进一步的更改或将某些函数限制在当前存在的数据类型。例如,使用类型编号作为输入的函数应替换为采用 DType 类的函数。虽然是公开的,但下游项目似乎很少(甚至从不)使用这部分 C-API 的大部分内容。

详细描述#

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

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

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

这种改变在 API 方面的主要后果是,特殊方法从 dtype 实例移动到新 DType 类的方法。这是 Python 中典型的设计模式。以更符合 Python 习惯的方式组织这些方法和信息,为将来完善和扩展 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 后端 (CuPy) 结合,用于存储与 GPU 相关的额外方法,而不是作为定义新数据类型的机制。类层次结构确实提供了价值,通过允许创建*抽象*数据类型可以实现这一点。抽象数据类型的一个例子是与 np.floating 等效的数据类型,代表任何浮点数。这些可以起到与 Python 的抽象基类相同的作用。

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

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

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

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

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

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

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

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

对于像 float64(见下文)这样的简单数据类型,似乎很诱人地认为 np.dtype("float64") 的实例可以是标量。这个想法可能更具吸引力,因为目前标量而非数据类型定义了有用的类型层次结构。

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

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

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

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

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

因此,新版本应替换目前用于定义新数据类型的 PyArray_ArrFuncs 结构。当前存在的且使用这些插槽定义的数据类型将在弃用期间得到支持。

为了向用户隐藏实现细节从而实现未来的可扩展性,最可能的解决方案是效仿 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 侧的方法,如 dtype.__dtype_method__,尽管向 Python 公开是后续步骤,以降低初始实施的复杂性。

UFunc 机制的 C-API 更改 (4)#

提议的对 UFunc 机制的更改将属于 NEP 43。但是,以下更改将是必要的(有关当前实现及其问题的详细说明,请参见 NEP 40

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

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

对通用函数的任何更改的一个重要目标将是允许重复使用现有的循环。对于一个新的单位数据类型,在处理完与单位相关的计算后,应该很容易回退到现有的数学函数。

讨论#

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

围绕本特定 NEP 的补充讨论已在邮件列表和 pull request 中进行

参考文献#

致谢#

为 NumPy 创建新数据类型的努力已经在许多不同背景和场合下讨论了数年,无法列出所有参与者。我们要特别感谢 Stephan Hoyer、Nathaniel Smith 和 Eric Wieser 对数据类型设计的多次深入讨论。我们非常感谢社区在审查和修订本 NEP 方面提供的意见,并特别感谢 Ross Barnowski 和 Ralf Gommers。