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数值类型,以及字符串、日期时间和结构化数据类型功能。然而,不断发展的 Python 社区需要更多样化的数据类型。例如,带有附加单位信息(例如米)的数据类型或分类数据类型(固定可能值的集合)。但是,当前的 NumPy 数据类型 API 限制了这些数据类型的创建。

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

动机和范围#

另请参阅

用户影响部分包含从长远来看,所提出的更改将启用哪些新型数据类型的示例。因此,它可能有助于按不同顺序阅读这些部分。

动机#

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

支持参数化数据类型的此附加功能增加了 NumPy 本身内部的复杂性,此外,外部用户定义的数据类型也无法使用。通常,不同数据类型的关注点没有得到很好的封装。内部 C 结构的公开加剧了这一负担,限制了新字段的添加(例如,支持新的排序方法[new_sort])。

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

  • 为参数化用户定义的 dtype 创建强制转换规则要么不可能,要么复杂到从未尝试过。

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

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

  • 在当前设计中,数据类型不能具有不适用于其他数据类型的方法。例如,单位数据类型不能具有.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 端的 dunder 方法。存储信息(如 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 机器以替换当前的调度和类型解析系统。旧系统应在一段时间内 *主要* 作为旧版本得到支持。

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

用户影响#

当前的生态系统很少使用 NumPy 的用户定义数据类型,其中两个最突出的类型是:rationalquaternion。这些表示相当简单的数据类型,不受当前限制的很大影响。但是,我们已经确定需要以下数据类型

  • bfloat16,用于深度学习

  • 分类类型

  • 物理单位(例如米)

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

  • 高精度数学

  • 专门的整数类型,如 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 的精度高于 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])

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

是否定义从一个值较少的分类数据类型到一个值严格较多的分类数据类型的转换,是分类数据类型需要决定的。两种选择都应该可用。

数据类型上的单位#

定义单位的方法有很多种,具体取决于内部机制的组织方式,一种方法是为每种现有的数值类型创建一个单位数据类型。这将写成 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 输出数据类型的确定可能比简单数值数据类型更复杂,因为没有“通用”的输出类型。

>>> 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() 进行数据类型转换以及 np.common_type 等相关操作的功能。

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

      • 创建一个公共 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 内部都没有使用)将不再受支持。

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

  • 数据类型实现者(C-API):

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

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

    • 目前用户数据类型定义为 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 和 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 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 建模 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 特定信息的功能。这需要修改内部循环函数签名,并在使用内部循环之前和之后添加新的挂钩。

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

讨论#

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

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

参考文献#

鸣谢#

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