NEP 51 — 更改 NumPy 标量的表示#

作者

塞巴斯蒂安·伯格

地位

公认

类型

标准轨道

创建

2022-09-13

解决

https://mail.python.org/archives/list/numpy-discussion@python.org/message/U2A4RCJSXMK7GG23MA5QMRG4KQYFMO2S/

抽象的

NumPy 具有标量对象(“NumPy 标量”),表示与 NumPy DType 相对应的单个值。这些的表示当前与 Python 内置函数的表示相匹配,给出:

>>> np.float32(3.0)
3.0

在此 NEP 中,我们建议更改表示形式以包含 NumPy 标量类型信息。将上面的例子改为:

>>> np.float32(3.0)
np.float32(3.0)

我们希望这一更改将帮助用户区分 NumPy 标量和 Python 内置类型并阐明它们的行为。

一旦采用NEP 50,NumPy 标量和 Python 内置函数之间的区别对于用户来说将变得更加重要。

这些变化确实导致了与阵列打印相关的较小的不兼容和基础设施变化。

动机和范围#

此 NEP 建议更改以下 NumPy 标量类型的表示形式,以将它们与 Python 标量区分开来:

  • np.bool_

  • np.uint8np.int8和所有其他整数标量

  • np.float16, np.float32, np.float64,np.longdouble

  • np.complex64, np.complex128,np.clongdouble

  • np.str_,np.bytes_

  • np.void (结构化数据类型)

此外,剩余 NumPy 标量的表示将被调整为打印为np.而不是numpy.

  • np.datetime64np.timedelta64

  • np.void (非结构化版本)

NEP 不建议更改这些标量的打印方式 - 仅__repr__更改它们的表示形式 ( )。此外,数组表示不会受到影响,因为它已经包含了dtype=必要时的。

更改背后的主要动机是 Python 数值类型的行为与 NumPy 标量不同。例如,应谨慎使用精度较低的数字(例如uint8float16),并且用户在使用它们时应注意。所有 NumPy 整数都会遇到溢出,而 Python 整数则不会。采用NEP 50时,这些差异将会加剧, 因为较低精度的 NumPy 标量将更频繁地被保留。 Evennp.float64与 Python 非常相似float并继承自 Python,但其行为确实有所不同,例如除以零时。

另一个常见的混淆来源是 NumPy 布尔值。 Python 程序员有时会编写,当一个显示为 的对象未能通过测试时,他们会感到惊讶。当值显示为 时,更容易理解此行为。obj is TrueTruenp.True_

我们不仅期望这一变化能够帮助用户更好地理解并提醒用户 NumPy 和 Python 标量之间的差异,而且我们还相信这种认识将极大地帮助调试。

使用和影响#

大多数用户代码不应受到更改的影响,但用户现在经常会看到 NumPy 值显示为:

np.True_
np.float64(3.0)
np.int64(34)

等等。这也意味着 Jupyter 笔记本单元中的文档和输出通常会完整地显示类型信息。

np.longdoublenp.clongdouble用单引号打印:

np.longdouble('3.0')

允许往返。除了这一更改之外,float128现在将始终打印,因为longdouble旧名称给人一种精确的错误印象。

向后兼容性#

我们预计大多数工作流程不会受到影响,因为只有打印发生变化。总的来说,我们认为在某些情况下,告知用户他们正在使用的类型比调整打印的需要更重要。

NumPy 测试套件包括诸如decimal.Decimal(repr(scalar)).需要修改此代码才能使用str().

一个例外是带有文档的下游库,尤其是文档测试。由于许多值的表示会发生变化,因此在许多情况下必须更新文档。预计中期需要更大规模的文档修复。

可能有必要采用文档测试测试工具来允许对新表示进行近似值检查。

更改为arr.tofile()#

arr.tofile()当前存储值与repr(arr.item())在文本模式下时相同。这并不总是理想的,因为这可能包括到 Python 的转换。一个问题是,这将开始保存 longdouble, np.longdouble('3.1')这显然是不希望的。我们预计这种方法很少用于对象数组。对于字符串数组,使用repr 也会导致存储"string"or ,b"string"这似乎很少需要。

建议将默认值(返回)更改为 usestr而不是 repr.如果repr需要,用户必须通过fmt=%r.

详细说明

该 NEP 建议将 NumPy 标量的表示形式更改为:

  • np.True_对于np.False_布尔值(它们的单例实例)

  • np.scalar(<value>),即np.float64(3.0)对于所有数值数据类型。

  • np.longdouble和 的值np.clongdouble将用引号括起来: np.longdouble('3.0')。这确保了它始终可以正确往返并匹配行为方式decimal.Decimal。对于这两个,将不会使用基于大小的名称,float128因为实际大小取决于平台,因此会产生误导。

  • np.str_("string")对于np.bytes_(b"byte_string")字符串数据类型。

  • np.void((3, 5), dtype=[('a', '<i8'), ('b', 'u1')])(类似于数组)用于结构化类型。这将是重新创建标量的有效语法。

与数组不同,标量表示应该正确往返,因此 longdouble 值将被引用,而其他值永远不会被截断。

在某些地方(即屏蔽数组、void 和记录标量),我们希望打印不带类型的表示形式。例如:

np.void(('3.0',), dtype=[('a', 'f16')])  # longdouble

应打印带引号的 3.0(以确保往返),但不重复完整内容,np.longdouble('3.0')因为 dtype 包含 longdouble 信息。为了实现这一点,将引入一个新的半公共np.core.array_print.get_formatter()来扩展当前的功能(请参阅实现)。

对屏蔽数组和记录的影响#

NumPy 的其他一些部分将被间接更改。屏蔽数组 fill_value将被调整为仅包含完整的标量信息,例如fill_value=np.float64(1e20)当数组的数据类型不匹配时。对于 longdouble (具有匹配的数据类型),它将被打印为 fill_value='3.1'包含引号(原则上但可能不会在实践中)确保往返。应该注意的是,对于字符串,数据类型在字符串长度上不匹配是典型的情况。因此,字符串通常会被打印为 np.str_("N/A").

np.record量将与其对齐np.void并以相同的方式打印(名称本身除外)。例如: np.record((3, 5), dtype=[('a', '<i8'), ('b', 'u1')])

有关longdoubleclongdouble# 的详细信息

对于longdoubleclongdouble值,例如:

np.sqrt(np.longdouble(2.))

除非以字符串形式引用,否则可能不会往返(因为转换为 Python 浮点数会丢失精度)。此 NEP 建议使用类似于 Python 的小数的单引号,其打印为Decimal('3.0')

longdouble可以有不同的精度和存储大小,从 8 到 16 字节不等。然而,即使float128是正确的,因为数字存储为 128 位,但它通常不具有 128 位精度。 (clongdouble相同,但存储大小是两倍。)

因此,该新经济政策包括将 的名称更改longdouble 为“始终打印为”longdouble且从不打印float128“或”的提议float96。它不包括弃用np.float128别名。然而,这种弃用可能独立于 NEP 发生。

整数标量类型名称和实例表示#

一个细节是,由于 NumPy 标量类型基于 C 类型,NumPy 有时会区分它们,例如在大多数 64 位系统(不是 Windows)上:

>>> np.longlong
numpy.longlong
>>> np.longlong(3)
np.int64(3)

该提案将导致longlong类型的名称,同时使用int64标量的形式。做出这种选择是因为int64通常对用户来说更有用的信息,但类型名称本身必须精确。

执行

笔记

这部分在最初的 PR中并未实现 。需要进行类似的更改来修复打印中的某些情况并允许完全正确的打印,例如包括长双精度的结构化标量。预计将来也需要类似的解决方案,以允许正确打印自定义 DType。

新的表示形式主要可以在标量类型上实现,并在测试套件中需要进行最大的更改。

对 void 标量和屏蔽的建议更改fill_value使得有必要在没有类型的情况下公开标量表示。

我们建议引入半公开API:

np.core.arrayprint.get_formatter(*,
        data=None, dtype=None, fmt=None, options=None)

替换当前的内部_get_formatting_func.与旧功能相比,这将允许两件事:

  • data可能None(如果dtype通过)允许不传递稍后将打印/格式化的多个值。

  • fmt=将来将允许将格式字符串传递给特定于 DType 的元素格式化程序。目前,get_formatter()将接受 repror str(单例而不是字符串)来格式化没有类型信息的元素('3.1'而不是np.longdouble('3.1'))。该实现确保格式匹配(类型信息除外)。

    空格式字符串的打印方式与str()(传递数据时可能有额外的填充)相同。

get_formatter()预计将来会查询用户 DType 的方法,从而允许对所有 DType 进行自定义格式设置。

公开get_formatter允许它用于np.record屏蔽数组。目前,格式化程序本身似乎是半公开的;使用单个入口点有望为格式化 NumPy 值提供清晰的 API。

标量表示变化的大部分内容之前是由 Ganesh Kathiresan 在[ 2 ]中完成的。

备择方案

可以考虑不同的表示形式:替代方案包括拼写 np.为数字标量numpy.或删除np.数字标量中的部分。我们相信使用np.足够清晰、简洁,并且允许复制粘贴表示。仅使用float64(3.0)不带np.前缀的方式更简洁,但可能存在 NumPy 依赖关系不完全清晰且名称可能与其他库冲突的上下文。

对于布尔值,替代方法是使用np.bool_(True)or bool_(True)。然而,NumPy 布尔标量是单例,并且建议的格式更简洁。布尔值的替代方案之前也在[ 1 ]中讨论过。

对于字符串标量,混乱通常不太明显。推迟更改这些可能是合理的。

非有限值#

该提案不允许复制粘贴naninf值。他们可以由np.float64('nan')或来代表np.float64(np.nan) 。这更简洁,Python 也使用nanandinf而不是通过将其显示为 来允许复制粘贴float('nan')。可以说,这将是 NumPy 中的一个较小的添加,其中 已经始终被打印。

get_formatter()#的替代方案

fmt=被传递时,专门用于主要用途(在此 NEP 中)格式化为 areprstr。还可以使用 ufunc 或直接格式化函数,而不是将其包装到`get_formatter()依赖于实例化 DType 格式化程序类的 中。

此 NEP 并不排除创建 ufunc 或创建特殊路径。但是,NumPy 数组格式化通常会查看要格式化的所有值,以便添加用于对齐的填充或提供统一的指数输出。在这种情况下data=通过并在准备中使用。遗憾的是,这种格式形式(与data=None所需的标量情况不同)从根本上与 UFunc 不兼容。

使用单例strandrepr确保将来的格式化字符串f"{arr:r}"不会以任何方式受到使用"r"or 的 限制"s"

讨论

参考文献和脚注#