ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

NEP 40 解读:NumPy 旧版数据类型(Legacy Datatype)实现的内部机制与演进动因

NEP 40 解读:NumPy 旧版数据类型(Legacy Datatype)实现的内部机制与演进动因 NEP 40 解读NumPy 旧版数据类型Legacy Datatype实现的内部机制与演进动因【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpyNEP 40NumPy Enhancement Proposal 40是由 Sebastian Berg 于 2019 年提出的 Informational 类型提案记录了 NumPy 1.18 时代 dtype数据类型系统的真实技术状态。本文以该提案为骨架结合当前仓库中的 C 源码ndarraytypes.h、convert_datatype.c 等逐层剖析参数化数据类型、基于值的转换、ufunc 分派与类型发现等核心机制帮助读者理解旧 dtype 为什么难扩展、新 dtype 系统NEP 41/42/43为什么这样设计并为阅读后续提案提供必要的背景知识。图NumPy 标量类型的层次结构摘自参考文档np.intp等别名未示出datetime 与 timedelta 未展示一、NEP 40 在提案系列中的定位NEP 40 是一系列提案的开篇提案仓库中的文档主题NEP 40本文nep-0040-legacy-datatype-impl.rst剖析现有 dtype 实现的各种不足NEP 41nep-0041-improved-dtype-support.rst替代方案的总体概览NEP 42nep-0042-new-dtypes.rst新设计中与 dtype 相关的 APINEP 43nep-0043-extensible-ufuncs.rst新设计中通用函数ufunc的 API正如提案 Abstract 所述NEP 40 的价值在于如实记录现状它详细描述了那些催生 41/42/43 的技术细节与概念。大多数读者应从 NEP 41 读起而把本文作为背景参考与细节索引。二、参数化数据类型Parametric Datatypes2.1 什么是参数化部分数据类型本质上是**参数化parametric**的所有np.flexible标量类型都挂在参数化 dtype 之下字符串S、字节串b/S、voidV。np.flexible是标量类层次中针对变长数据类型的超类。这一区分同样体现在 C 宏上见 ndarraytypes.h#define PyTypeNum_ISFLEXIBLE(type) (((type) NPY_STRING) \ ((type) NPY_VOID))以及派生出的PyDataType_ISFLEXIBLE(obj)ndarraytypes.h与PyArray_ISVARIABLE(obj)ndarraytypes.h后者在注释中明确承认该检查被硬编码在内置数据类型上并非通用的是否变长标志——这正是旧实现的一个结构性局限。参数化的含义会推广到数组中可表示的值集合例如S8能表示比S4更长的字符串因此参数化字符串 dtype 同时也把数组内的值限制为字符串标量全部值的一个子集subtype。2.2 数值 dtype 不灵活但 datetime 是隐藏的参数化基本数值 dtypefloat64、float32等不继承np.flexible它们虽带字节序但所描述的值不受字节序影响且总能无损地转换到原生规范表示canonical representation。然而灵活性可以推广私有函数PyArray_AdaptFlexibleDType也接受朴素naivedatetime dtype 作为输入以确定正确的时间单位。也就是说datetime dtype 的参数化不在于存储大小而在于存储的值代表什么。这带来一个语义疑点当前np.can_cast(datetime64[s], datetime64[ms], castingsafe)返回True但这是否合理、能否推广到未来的物理单位等数据类型并不明确。2.3 参数化 dtype 引发的三个性质以字符串为代表的 dtype 因而具有以下特性转换并非总是安全np.can_cast(S8, S4)不安全会丢信息。数组强制转换应能发现精确 dtype如np.array([str1, 12.34], dtypeS)中 NumPy 会推断出结果为S5若省略 dtype 参数该行为目前定义不明确对应 GitHub issue gh-15327。类似地dtypedatetime64可以自动发现单位np.array([2017-02], dtypedatetime64)。正是这种复杂性直接导致了 ufunc 输出类型推断的复杂化。三、基于值的转换Value Based Casting3.1 机制与示例转换通常定义在两个类型之间若第二类型能无损表示第一类型的所有值则称第一类型可安全转换到第二类型。而 NumPy 的一个特殊之处在于它会检查实际值来决定转换是否安全。例如arr np.array([1, 2, 3], dtypeint8) result arr 5 assert result.dtype np.dtype(int8) # 如果值更大结果会改变 result arr 500 assert result.dtype np.dtype(int16)这里 Python 值原本没有 dtype被表示为最小的合适类型5可放进int8而500必须用int16。值得注意的细节是NumPy 对 NumPy 标量和零维数组也执行同样的逻辑——把上面的5换成np.int64(5)或np.array(5, dtypeint64)结果不变即忽略了已有的 dtype。浮点标量同样适用且允许丢失精度。但当两个输入都是标量时该行为不生效5 np.int8(5)返回默认整型大小32 或 64 位而不是np.int8。该行为虽以转换语义定义并经np.result_type暴露其主要意义在 ufunc如上面np.add的例子ufunc 依赖安全转换语义来选择内层循环从而决定输出 dtype。3.2 讨论社区大致认可对已经带 dtype 的值执行基于值的转换并不理想但对纯 Python 整数/浮点如第一个例子可能有用。不过任何对 dtype 系统与 ufunc 分派的改动都必须先完整兼容当前行为。一个主要难点是比如值156既可用np.uint8表示也可用np.int16表示结果取决于转换上下文中的最小表示对 ufunc 而言上下文可能取决于循环顺序。四、对象数据类型The Object Datatypeobjectdtype 目前充当任何无法用其他方式表示的值的通用回退。但由于其类型定义不完善存在不少问题例如用 Python 序列填充数组时 l [1, [2]] np.array(l, dtypenp.object_) array([1, list([2])], dtypeobject) # 一维数组 a np.empty((), dtypenp.object_) a[...] l ValueError: assignment to 0-d array # ??? a[()] l a array(list([1, [2]]), dtypeobject)没有明确定义的类型isnan()、conjugate()等函数不一定能工作但对decimal.Decimal这类对象可以。为改善现状提案认为应当易于创建表示特定 Python 类型的 object dtype让数组以PyObject指针形式存储对象。与多数 dtype 不同Python 对象需要垃圾回收因此必须额外定义引用处理与遍历所有对象的方法。在实践中多数使用场景只要限制这类 dtype 的创建方式使所有与 Python C 级引用相关的功能对 NumPy 保持私有即可。创建与内置 Python 对象匹配的 dtype 还引出两个不必立刻解决的难题某些场景下 NumPy 对数组输入也会返回标量通常无缝工作——但这只是因为 NumPy 标量行为很像 NumPy 数组普通 Python 对象不具备该特性无缝集成可能要求np.array(scalar)能自动找到正确的 DType因为索引等操作返回的是标量而非 0D 数组如果多个用户各自独立实现decimal.Decimal的 DType就会产生冲突。五、当前 dtype 实现np.dtype、单例与PyArray_ArrFuncs5.1 全局原型实例与单例模式目前np.dtype是一个 Python 类其实例即np.dtype(float64)等。为设定这些实例的实际行为系统在全局保存一个原型实例prototype instance依据dtype.typenum查找能复用单例就复用必要时如改字节序复制并修改。参数化 dtype字符串、void、datetime、timedelta必须额外存储字符串长度、字段、datetime 单位等信息因此会新建实例而非依赖单例。所有当前内置 dtype 还支持在创建时设置 metadata 字段任意字典值但实践中很少用h5py 是近期一个知名用户。5.2PyArray_ArrFuncsdtype 上的函数表大量 dtype 专属函数定义在名为PyArray_ArrFuncs的 C 结构体中见 ndarraytypes.h它是每个 dtype 实例的一部分与 Python 的PyNumberMethods有相似之处。该结构保存了诸如如何复制、如何转换等重要信息并为比较元素、转 bool、排序等函数指针预留空间它作为f字段挂在PyArray_Descr上ndarraytypes.h。对用户自定义 dtype该结构直接暴露给用户这使ABI 兼容性改动成为不可能。由于其中部分函数是向量化操作一次处理多个元素它们其实更契合 ufunc 模型未来不必定义在 dtype 上。例如np.clip原先用PyArray_ArrFuncs实现如今已改为 ufunc 实现。5.3 讨论函数不接收 dtype 实例当前 dtype 上函数的另一个问题是与方法不同调用时不会传入 dtype 实例很多情况下传入的是被操作的数组且通常只用于重新提取 dtype。未来 API 应停止传入完整数组对象。由于向后兼容需要回退到旧定义数组对象可能不可用但传入一个主要定义好 dtype 的假数组或许是足够的变通还需考虑对齐信息。PyArray_Descr目前是公共结构PyArray_ArrFuncs亦如此虽未在 NumPy 之外被大量使用但由于兼容性它们可能需要被支持很长时间可能的路径是用分派到新 API 的函数替代它们长期来看对这些结构的直接访问大概需要废弃deprecate。六、NumPy 标量与类型层次与 dtype 不同NumPy 标量确实提供了类型层次由np.inexact等抽象类型组成见文首图片。实际上 NumPy 内部的部分控制流目前就在使用issubclass(a.dtype.type, np.inexact)。NumPy 标量试图模仿固定 dtype 的零维数组对数值及 unicodedtype 而言它们进一步限制为原生字节序。七、当前转换casting实现7.1 转换表与参数化特例dtype 需要支持的核心功能之一是在类型间转换arr.astype(new_dtype, castingunsafe)或在 ufunc 执行过程中对不同类型如整数与浮点相加进行转换。转换表casting tables决定能否从某具体类型转换到另一具体类型但通用转换规则无法处理字符串这类参数化 dtype——参数化 dtype 的逻辑主要定义在PyArray_CanCastTo中convert_datatype.c其实现委托给PyArray_CanCastTypeTo(..., NPY_SAFE_CASTING)且目前无法为用户自定义 dtype 定制。7.2 三阶段转换链实际转换由两个不同部分构成copyswap/copyswapn每个 dtype 都定义负责处理非原生字节序的字节交换以及未对齐内存通用转换代码由一组知道如何在原生字节序下把对齐且连续的内存从一种 dtype 转到另一种的 C 函数提供。这些 C 级函数可被注册调用时可能传入两个数组对标量该参数有时为NULL。NumPy 会保证这些函数收到原生字节序输入。当前实现把函数存于被转换 dtype 上的 C 数组中或当转到用户自定义 dtype 时存于字典中。因此 NumPy 一般把转换表示为三个函数的链in_copyswapn - castfunc - out_copyswapn步骤之间使用小型缓冲区。上面的多个函数会被包装成单个带元数据的函数用于例如 ufunc 的缓冲迭代——这是用户自定义 dtype 始终走的路。对 NumPy 内部的大多数 dtype则使用更专门的代码来寻找实际转换函数由私有函数PyArray_GetDTypeTransferFunction定义该机制替换了上述大部分流程在输入非连续内存时提供快得多的转换但无法被用户自定义 dtype 扩展。与转换相关还有一个PyArray_EquivTypes函数用于判断仅需 view 即可无需转换它在多处被使用理应成为重构后转换 API 的一部分。八、ufunc 中的 DType 处理8.1ufunc.types与 type_resolverufunc 是numpy.UFunc类的实例持有一个有序的、dtype 专属实现列表按 dtype 的 typecode 字符而非 dtype 实例区分每个实现带签名与函数指针 np.add.types [..., ll-l, ..., dd-d, ...]每个签名关联一个在 C 中定义的内层循环函数它做实际计算且可能被多次调用。找到正确内层循环的关键步骤是调用PyUFuncObject.type_resolver即PyUFunc_TypeResolutionFunc它从输入数组取得输入 dtype并确定要执行的完整类型签名含输出 dtype。默认TypeResolver的实现是按ufunc.types中列出的顺序依次搜索所有实现一旦发现所有输入都能安全转换以适配该签名即停止。这意味着 longl与 doubled数组相加时NumPy 会找到dd-d定义long 可安全转换到 double并使用之。8.2 自定义 TypeResolver 与下游实践有些场景并不希望默认行为例如np.isnatufunc 的TypeResolver会拒绝整数输入而不是允许其转换到 float。原则上下游项目目前可以使用自己的非默认TypeResolver相应 C 结构是公开的已知这样做的唯一项目是 Astropy并且如果 NumPy 移除替换 TypeResolver 的可能性Astropy 愿意切换到新 API。用户自定义 dtype 的分派逻辑类似但独立实现且受限见下。8.3 用户自定义 dtype 分派的局限用户自定义函数只有在**任一输入或输出**是用户 dtype 时才能被解析到因为它依赖OO-O签名。例如即使已为fraction_divide(int, int) - Fraction实现了 ufunc 循环fraction_divide(4, 5)未指定输出 dtype仍会失败——因为只有输入已是Fraction时才能找到含该用户 dtype 的循环fraction_divide(4, 5, dtypeFraction)可以工作但不方便。分派通常是找到第一个匹配的循环匹配定义为所有输入可能还有输出都能安全转换到签名 typecode。但某些情况下安全转换是有问题的、被显式禁止的例如np.isnat目前只为 datetime 与 timedelta 定义尽管整数被定义为可安全转换到 timedelta。若不加限制np.isnat(np.array(NaT, timedelta64).astype(int64))会返回 true——但整数输入数组根本没有非时间NaT的概念。若某 ufunc如scipy.special中多数函数只为float32/float64定义目前会静默把float16以及任何整数输入转换到float32。这保证执行成功但当该 ufunc 新增数据类型支持时会导致输出 dtype 改变一旦加入float16循环输出 dtype 会从float32变成float16且无任何警告。此外循环注册顺序很重要但这只有在所有循环都在 ufunc 首次定义时加入才可靠导入新用户 dtype 时附加的循环必须不依赖于导入顺序。针对用户自定义类型提案给出两个改进方向允许用户 dtype 直接影响循环选择例如在无精确匹配循环时提供返回/选择循环的函数定义所有实现/循环的全序可能基于安全转换语义或类似语义。方案 2 更易推理但能否覆盖全部或大多数用例仍有待观察。九、ufunc 中参数化输出 DType 的调整参数化 dtype 需要的第二个步骤目前也在TypeResolver内完成datetime 与 timedelta 必须为操作与输出数组决定正确的参数单位同时还需复查所有转换是否安全默认即same kind转换。讨论指出确定正确的输出 dtype 目前是类型解析的一部分但它其实是独立的一步应在实际类型/循环解析之后单独处理。因此该步骤将来可能从分派阶段迁到下文所述的实现专属代码中。十、ufunc 的 DType 专属实现内层循环函数的局限找到正确实现/循环后ufunc 调用单个用 C 编写的内层循环函数它可能被多次调用以完成全部计算对当前上下文几乎一无所知且返回void。由此带来一系列问题参数信息传递缺失。参数化 dtype 可能需要向内层循环传入额外信息以解释数据——这正是目前没有字符串 dtype 的 ufunc 的原因虽然在 NumPy 内部技术上可行。目前可以向循环传入输入数组对象无需转换时数组携带 dtype但这要求传入完整数组信息且数组是在任何转换发生之前传入的该特性在 NumPy 内部未使用也没有已知用户。错误报告机制受限。内层循环中报告错误目前有两种方式设置 Python 异常或使用 CPU 浮点错误标志。两者都会在返回用户前被检查。但许多整数函数两种错误都无法设置检查浮点错误标志就成了不必要的开销另一方面也没有不借助浮点标志或不需要持有 GIL 就能停止迭代、传出错误信息的方法。因此有必要给内层循环作者更多控制权允许更容易地传入/传出信息同时不提供输入数组对象。最可能涉及允许在内层循环第一次调用前与最后一次调用后执行附加代码内层循环返回整数值以便提前停止迭代并可能传播错误信息可能允许专门化的内层循环选择例如目前matmul与许多归约会为特定输入执行优化代码预先选择这类优化循环可能是有益的也有助于拉近转换重度使用该机制与 ufunc 实现之间的距离。围绕内层循环的这些问题在 GitHub issue gh-12518 中有详细讨论。归约的 identity 值。归约使用identity值目前是每个 ufunc 定义一次与 ufunc 的 dtype 签名无关如sum用0min用math.inf。这对数值 dtype 很合适但对其他 dtype 并非总是恰当一般而言应能向 ufunc 归约提供dtype 专属的 identity。十一、数组强制转换时的数据类型发现调用np.array(...)把普通 Python 对象强制转换为 NumPy 数组时需要检查所有对象以找到正确的 dtype。输入是可能嵌套的 Python 序列最终元素是普通 Python 对象NumPy 必须解包所有嵌套序列并检查元素。最终 dtype 通过遍历所有最终进入数组的元素获得发现单个元素的 dtype对数组或类数组或 NumPy 标量使用element.dtype对已知 Python 类型使用isinstance(..., float)注意这些规则意味着子类目前有效对 void dtype 有特殊规则以强制转换元组。用np.promote_types将当前 dtype 与下一元素 dtype 进行提升promote。若发现字符串整个过程重新开始参见 gh-15327方式与给定dtypeS类似见下。若给了dtype...除非它是不具体的参数化 dtype 实例——即S0、V0、U0、datetime64、timedelta64长度 0 的灵活 dtype视为未定大小以及未挂单位、即通用单位的 datetime/timedelta——否则该 dtype 原样使用。在未来 DType 类层次中这些可能用类本身而非特殊实例表示因为这类特殊实例通常不应挂到数组上。若提供了这样的参数化 dtype 实例如dtypeS则会调用PyArray_AdaptFlexibleDType用 DType 专属逻辑有效检查所有值字符串用str(element)求得大多数元素的长度Datetime64能从字符串强制转换并猜测正确单位。从当前源码可以看到这段逻辑的去向在 convert_datatype.c 中cast_to_string_resolve_descriptors的注释明确指出其代码曾属于PyArray_AdaptFlexibleDType——例如把 bool 转为True/False需要 5 个字符、无符号整数按REQUIRED_STR_LEN决定长度、有符号整数加 1 个符号位、float/double 取 32、longdouble 取 48、复数按两倍实部长度计算等。这展示了新 dtype 系统中字符串长度推断如何从旧 API 迁移为转换描述符解析逻辑。讨论认为常规发现过程中的isinstance或许应改为严格的type(element) is desired_type检查此外当前的AdaptFlexibleDType逻辑应开放给用户 DType且不应作为次要步骤而应取代或并入常规发现流程。十二、相关问题与相关工作相关问题。np.save目前把所有用户自定义 dtype 都转成 void dtype因此它们无法用npy格式存储。对 Python pickle 协议这不是问题但若想在不执行恶意代码的前提下安全加载这类文件即不使用allow_pickleTrue则需要更多思考。另外Pandas 中掩码数组尤其掩码 dtype的存在对互操作性有有趣的影响掩码信息常单独存储其处理需要容器数组对象支持NumPy 自身不提供也不预期在可预见的未来添加这种支持但如果 dtype 层的此类添加能改善互操作性即使 NumPy 自身不用也可以考虑。相关工作。提案将以下外部工作视为参照系仅作背景说明不构成仓库内事实Julia 的类型系统定义了抽象与具体类型是类型层次的良好蓝图Julia 的提升promotion可基于抽象类型发生定义提升器后先转换输入再重试寻找实现xnd-projectndtypes 与 gumath与 NumPy 类似地定义数据类型并支持扩展但主要差异在于它不在 ufunc 内部使用提升/转换而是要求显式定义int32 float64 - float64之类的循环。十三、结语旧实现如何推动新设计NEP 40 的核心信息可以浓缩为几点参数化 dtype 需要独立的调整参数步骤基于值的转换与 ufunc 分派深度耦合PyArray_ArrFuncs作为公开结构锁死了 ABI用户 dtype 在 ufunc 分派、转换注册、数组强制转换中发现三个层面均受限于硬编码逻辑。这些问题在后续 NEP 41/42/43仓库中对应 nep-0041-improved-dtype-support.rst、nep-0042-new-dtypes.rst、nep-0043-extensible-ufuncs.rst中被逐一回应以 DType 类层次取代 typecode 单例、以公开的转换描述符resolve_descriptors取代PyArray_ArrFuncs、以可注册的循环选择规则取代线性扫描。理解 NEP 40 所描述的旧世界是理解当前 NumPy dtype 架构演进的关键起点。【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进