多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

NumPy NEP 51 深度解析:标量 repr 变更的设计动机与源码实现

NumPy NEP 51 深度解析:标量 repr 变更的设计动机与源码实现 NumPy NEP 51 深度解析标量 repr 变更的设计动机与源码实现【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpyNumPy 2.0 起标量在交互式环境中不再伪装成 Python 内建类型np.float32(3.0)的repr变成了np.float32(3.0)而不是3.0。这项变更由标准轨道提案 NEP 51状态Accepted定义其目的是让 NumPy 标量与 Python 内建标量在外观上可区分从而暴露精度、溢出和类型差异等隐蔽行为。本文完整继承 NEP 51 的提案内容并结合当前仓库中 scalartypes.c.src 与 arrayprint.py 的实际实现讲清每一项 repr 规则是如何落地的、哪些影响会波及下游代码以及如何用legacy打印选项回退到旧行为。一、背景与动机为什么标量要表明身份1.1 变更范围NEP 51 提出的 repr 变更覆盖以下 NumPy 标量类型np.bool_单例np.True_/np.False_np.uint8、np.int8及所有其他整型标量np.float16、np.float32、np.float64、np.longdoublenp.complex64、np.complex128、np.clongdoublenp.str_、np.bytes_np.void结构化 dtype 版本此外剩余标量的 repr 只是把前缀从numpy.统一改为np.行为本身不变np.datetime64与np.timedelta64np.void非结构化的原始字节版本提案同时明确了两条边界只改__repr__不改__str__即打印输出print()的内容不变数组的 repr 不受影响因为数组 repr 在必要时已经携带dtype信息。1.2 动机Python 数值类型 ≠ NumPy 标量提案给出的核心论点是Python 数值类型与 NumPy 标量的行为差异会被相同的外表掩盖低精度类型需要警惕。uint8、float16等低精度标量应当被谨慎使用用户需要看见自己正处在低精度世界中。整数溢出。所有 NumPy 整型都会溢出而 Pythonint不会——np.uint8(255) 1得到np.uint8(0)。旧 repr 下这个结果只显示为0溢出的事实被完全隐藏。即使最像的np.float64也有行为差异。它继承自 Pythonfloat但除零等边界行为并不相同例如np.float64(1.0) / np.float64(0.0)得到inf并伴随浮点错误而不是ZeroDivisionError。布尔值陷阱。Python 程序员习惯写obj is True而np.bool_标量is True会失败当它显示为np.True_时这种单例身份的差异才变得一目了然。这些差异在 NEP 50标量提升规则变更被采纳后会进一步放大低精度标量会在二元运算中被更频繁地保留下来uint8、float16等值在结果中出现的频率显著上升此时 repr 携带类型信息对调试的帮助是决定性的。二、新 repr 规则完整映射与 round-trip 原则2.1 类型 → 新 repr 映射标量类型旧 repr新 reprnp.bool_单例True/Falsenp.True_/np.False_整型标量如np.int64(34)34np.int64(34)、np.uint8(3)np.float16/np.float32/np.float643.0np.float32(3.0)np.longdouble/np.clongdouble3.0np.longdouble(3.0)带单引号np.complex64/np.complex128(34j)np.complex128(34j)np.str_/np.bytes_string/bbyte_stringnp.str_(string)/np.bytes_(bbyte_string)np.void结构化(3, 5)np.void((3, 5), dtype[(a, i8), (b, u1)])np.datetime64/np.timedelta64numpy.datetime64(...)np.datetime64(...)仅前缀改名np.void非结构化void(b\x01...)np.void(b\x01...)仅前缀改名与数组不同标量 repr 必须可以 round-tripeval(repr(x))应能重建原值。由此推出两条规则longdouble值必须加引号np.longdouble(3.0)。因为转成 Pythonfloat字面量会丢精度加引号保证按字符串重新构造行为对齐 Python 的DecimalDecimal(3.0)。其他数值类型永远不截断输出完整数字。2.2 longdouble 的细节引号 永不使用 float128 命名NEP 51 顺带提出一项命名修正longdouble的存储大小因平台而异8 到 16 字节不等即使某平台上它确实是 128 bit 存储也通常不具备 128 bit 精度clongdouble存储大小为其两倍精度同样不翻倍。因此 repr 一律显示为longdouble绝不显示float128或float96——尺寸式命名会给出虚假的精度印象。提案明确指出这不包含对np.float128别名本身的弃用该弃用可能独立于本 NEP 发生。一个典型例子是np.sqrt(np.longdouble(2.))结果无法用 Pythonfloat字面量无损表示只有以带引号字符串的形式单引号模仿decimal输出才能保证 round-trip。2.3 非有限值提案明确不支持直接复制粘贴nan/infrepr(np.float64(np.nan))显示为np.float64(nan)其中的nan不是合法 Python 字面量。替代写法是np.float64(nan)或np.float64(np.nan)。理由Python 内建float(nan)同样是靠nan这个名字而非可粘贴字面量而 NumPy 2.0 之后标量类型名总会出现在 repr 中这一取舍代价很小。三、源码实现scalartypes.c.src 中的 repr 机制NEP 51 的实现主体位于 C 层模板文件 scalartypes.c.srcTempita 模板按类型展开生成各标量类的tp_repr/tp_str入口。理解它的关键是一个版本闸门。3.1 legacy print mode 闸门几乎所有新 repr 代码都先调用get_legacy_print_mode()读取np.set_printoptions(legacy...)的数值再判断// numpy/_core/src/multiarray/scalartypes.c.src (整数 repr, L593-L629 节选) static PyObject * genint_type_repr(PyObject *self) { PyObject *value_string genint_type_str(self); ... int legacy_print_mode get_legacy_print_mode(); ... if (legacy_print_mode 125) { return value_string; // 旧风格只有数值 } ... if (PyTypeNum_ISUNSIGNED(num)) { repr PyUnicode_FromFormat(np.uint%d(%S), bitsize, value_string); } else { repr PyUnicode_FromFormat(np.int%d(%S), bitsize, value_string); } ... }可以看到源码中存在三档版本阈值 113走 1.13 时代的旧格式函数文件 L1170-L1334 有一段专门标注为 LEGACY PRINTING MODE CODE 的复刻代码 125走 1.25 风格的无类型 repr 125才启用 NEP 51 的新风格。这正是用户执行np.set_printoptions(legacy1.25)即可整体回退旧标量 repr 的底层原因见第七节。浮点科学计数法切换阈值也受版本控制nametype_kind_eitherL1356-L1390中legacy ≤ 202 时所有浮点统一以1e16为位置/科学记数分界新规则下模板参数#max_positional 1.e6L, 1.e16L, 1.e16L#分别对应 float、double、longdouble——即np.float32在 |x| ≥ 1e6 时切换为科学记数np.float64/np.longdouble在 |x| ≥ 1e16 时切换。3.2 整型位宽命名且区分 C 类型名整型 repr 通过_typenum_fromtypeobj拿到类型编号再经PyArray_DescrFromType计算bitsize elsize * 8最终输出np.uint%d/np.int%d格式串L608-L628。注意分支顺序num NPY_NOTYPE例如用户自定义标量时退回tp_name(value)的通用形式。这里落地了 NEP 中整型标量类型名与实例表示一节的设计NumPy 标量类型基于 C 类型np.longlong这样的类型名在多数 64 位系统Windows 除外与np.int64指向同一底层类型。提案的取舍是——类型对象保留精确的 C 名np.longlong标量实例统一用位宽名np.longlong(3)的 repr 是np.int64(3)因为int64对使用者的信息量更大。3.3 布尔单例直接返回固定字符串布尔最简单genbool_type_repr 在legacy_print_mode 125时直接返回字符串常量np.True_/np.False_读obval判断legacy ≤ 125 时回退为genbool_type_str的True/False。这也印证了 NEP 中布尔标量是单例故np.True_比np.bool_(True)更简洁的替代方案讨论结论。3.4 浮点与复数Dragon4 最短 round-trip 格式串模板数值部分的字符串由format_nameL720-L735生成内部调用 Dragon4 算法的Dragon4_Scientific_Name/Dragon4_Positional_Name并使用DigitMode_Unique模式——即生成能唯一还原该值的最短十进制串这是标量 repr 必须 round-trip原则的算法保障。repr 包装则通过模板参数完成L1337-L1354* #repr_format np.float32(%S), np.float64(%S), np.longdouble(%S)# * #crepr_imag_format np.complex64(%Sj), np.complex128(%Sj), * np.clongdouble(%Sj)# * #crepr_format np.complex64(%S%Sj), np.complex128(%S%Sj), * np.clongdouble(%S%Sj)#注意np.longdouble(%S)的单引号正是 NEP 中 round-trip 要求的直接体现复数有两条格式串纯虚部实部为 0走crepr_imag_format如np.complex128(4j)一般情形走crepr_format如np.complex128(34j)。当legacy_print_mode 125时才套上这层外壳L1400-L1411否则退回裸数值字符串。3.5 字符串、void、datetimeC 与 Python 的分层协作字符串标量stringtype_repr模板L744-L830重写 str/repr先剥离内部 NUL 码点再委托给PyUnicode/PyBytes的原生 repr 逻辑并套上np.str_(/np.bytes_(前缀。void 标量voidtype_repr 分两种情况。结构化 dtypePyDataType_HASFIELDS时回调到 Python 侧的_void_scalar_to_string非结构化时按 legacy 版本决定输出np.void(b...)新还是void(b...)旧。结构化 repr 的 Python 侧实现在 arrayprint.py 的_void_scalar_to_string先检查 legacy 版本 125走旧式StructuredVoidFormat新式则复用逐元素 formatter 拼出元组表示再构造完整 repr——注意它用cls.__module__.replace(numpy, np)把模块名归一为np.并显式携带 dtypecls_fqn cls.__module__.replace(numpy, np) . cls.__name__ void_dtype np.dtype((np.void, x.dtype)) return f{cls_fqn}({val_repr}, dtype{void_dtype!s})这就是 NEP 中类似数组、且是合法重建语法的目标形态np.void((3, 5), dtype[(a, i8), (b, u1)])的出处。由于np.record与np.void共用该路径仅类名不同np.record的 repr 也自动对齐为np.record((3, 5), dtype...)。datetime64 / timedelta64datetimetype_repr/timedeltatype_reprL930 起负责前缀由numpy.改为np.的调整数值格式不变。NEP 还提到一个展示细节在掩码数组、void/record 标量等场景需要无类型的内层格式——例如结构体中若某字段是 longdoublenp.void((3.0,), dtype[(a, f16)])应打印带引号的3.0保证 round-trip但不再重复np.longdouble(3.0)dtype 信息已经包含它。_void_scalar_to_string中options[formatter].setdefault(float_kind, str)等逐元素 formatter 机制承担的就是这类内层格式化。四、连带变更tofile、掩码数组 fill_value 与 np.record4.1arr.tofile()默认写 str 而非 reprNEP 51 指出了一个连锁问题tofile()文本模式过去以repr(arr.item())落盘新 repr 生效后 longdouble 会被写成np.longdouble(3.1)字符串数组会带上string引号显然都不是期望的文本。提案决定把默认改回为str需要 repr 的用户显式传%r。当前源码中array_tofile 的关键字签名是{file, sep, format}format默认值为空字符串char *sep ; char *format ; static char *kwlist[] {file, sep, format, NULL};即默认行为走str风格要恢复 repr 落盘应写arr.tofile(out.txt, format%r)NEP 原文写作fmt%r实际参数名以源码为准是format。4.2 掩码数组 fill_value 与 record掩码数组的fill_value显示被调整为仅当数组 dtype 与 fill_value 标量不匹配时才输出完整类型信息如fill_valuenp.float64(1e20)longdouble 在 dtype 匹配时打印为带引号的fill_value3.1原理上、实践中未必严格 round-trip。字符串因 dtype 长度常常不匹配通常显示为np.str_(N/A)。从源码结构看这部分显示逻辑位于 numpy/ma/core.py 的MaskedArray格式化路径。np.record标量与np.void对齐除类名外打印完全一致见 3.5 节的公共实现路径。五、get_formatter()被提出但尚未落地的半公开 APINEP 51 的 Implementation 一节提出引入半公开接口以取代内部_get_formatting_funcnp.core.arrayprint.get_formatter(*, dataNone, dtypeNone, fmtNone, optionsNone)相比旧函数它新增两点能力data可为None前提是传dtype允许先取 formatter、之后批量格式化不必一次性传入全部待格式化值fmt接收格式化函数目前接受repr/str两个单例不是字符串r/s用于产出不含类型信息的元素格式3.1而非np.longdouble(3.1)空格式串等价于str()传data时可能附加对齐补白。设计取舍上有两点值得注意其一选择repr/str单例而非字符串是为将来f{arr:r}这类格式串预留空间——占位符r/s不会被格式串语义占用其二提案明确不改用 ufunc 或直调格式化函数数组格式化通常需要预读全部值做对齐补白或统一科学计数此时data被用于预备计算这种跨值状态与 ufunc 的逐元素模型根本不兼容。当前仓库的落地状态NEP 自身标注了 Implementation 一节未在初始 PR 中实现需要类似改动来修复结构化标量中包含 longdouble 等打印场景未来自定义 DType 的正确打印也需要同类方案。经全仓库检索确认当前代码中尚不存在名为get_formatter的函数该名称仅出现在 NEP 51 文档 本身其无类型内层格式的职责目前由 arrayprint.py 内部的_void_scalar_to_string与逐元素 formatter如StructuredVoidFormat承担。因此引用NEP 51 的get_formatter时应理解为提案方向而非现成 API。六、备选方案与取舍NEP 的 Alternatives 一节记录了三组被讨论掉的方案理解它们能解释最终设计前缀选np.而非numpy.或无前缀。np.足够清晰、简短且保证 repr 可直接复制粘贴若只用float64(3.0)更省字符但存在 NumPy 依赖不明确的上下文且名称可能与其他库冲突。布尔不用np.bool_(True)/bool_(True)。NumPy 布尔标量是单例np.True_更简洁该方向最早只有一个仅改布尔的早期 PRissue #12950 / PR #17592后被扩展为覆盖全部或至少绝大多数标量的实现PR #22449。字符串标量可以延后。字符串的混淆程度相对较低推迟变更在技术上说得通但提案将其一并纳入以保持类型信息的完整性。七、实践要点回退旧显示与测试适配7.1 用 legacy 打印选项回退由于每个 repr 入口都以get_legacy_print_mode()的阈值分支受控整型/布尔/浮点在 125 处分支格式细节在 113 与 202 处分支用户只需import numpy as np np.set_printoptions(legacy1.25) # 恢复 NEP 51 之前的标量 repr print(repr(np.float32(3.0))) # 3.0 print(repr(np.bool_(True))) # True源码中legacy_print_mode 125的分支如 genint_type_repr L604-L606正是这一回退开关的实现。注意str()输出从未被 NEP 51 改变回退只影响repr()及直接展示 repr 的交互式环境。7.2 下游代码与文档测试的适配NEP 明确预期了兼容性影响只用str()的工作流基本不受影响。典型需要修改的是把repr(scalar)喂给解析器的代码例如 NumPy 测试套件中的decimal.Decimal(repr(scalar))——repr(np.longdouble(3.1))现在是np.longdouble(3.1)直接解析会失败改用str(scalar)即可。文档与 doctest 是最大的中期成本。大量下游库文档里的输出示例会因 repr 变化而过期预期需要一轮较大规模的文档修订提案还建议引入支持近似值检查的 doctest 工具来匹配新表示。仓库内的验证入口标量打印行为的回归测试集中在 numpy/_core/tests/test_arrayprint.py、numpy/_core/tests/test_longdouble.py 等测试文件中断言大量形如np.float64(、np.int64(的新风格 repr可作为各类型新行为的权威示例集。小结NEP 51 是一次只动__repr__的小切口变更却系统性地解决了 NumPy 标量长期看起来像 Python 内建类型的认知陷阱整型以位宽自报家门np.int64(34)浮点靠 Dragon4 最短 round-trip 保证可重建longdouble 以引号隔离精度、以平台无关名消除误导布尔以单例名暴露is语义。源码侧这一切由 scalartypes.c.src 中受legacy版本闸门控制的一组 repr 模板实现结构化类型经 arrayprint.py 的 Python 回调完成tofile默认回退str则是它最直接的连带修正。对使用者最重要的两条实用结论是新 repr 都是合法可粘贴表达式而np.set_printoptions(legacy1.25)是回到旧显示的唯一开关。【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址: https://gitcode.com/gh_mirrors/nu/numpy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表