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

作者:

Sebastian Berg

状态:

已接受

类型:

标准跟踪

创建:

2022-09-13

解决:

https://mail.python.org/archives/list/[email protected]/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.float16np.float32np.float64np.longdouble

  • np.complex64np.complex128np.clongdouble

  • np.str_np.bytes_

  • np.void(结构化 dtypes)

此外,其余 NumPy 标量的表示形式将被调整为以 np. 而不是 numpy. 打印。

  • np.datetime64np.timedelta64

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

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

更改背后的主要动机是 Python 数值类型与 NumPy 标量行为不同。例如,应谨慎使用低精度数字(例如 uint8float16),并且用户应意识到他们在使用它们时。所有 NumPy 整数都可能发生溢出,而 Python 整数则不会。这些差异在采用 NEP 50 时会更加严重,因为更低精度的 NumPy 标量会更频繁地保留下来。即使是 np.float64,它与 Python 的 float 非常相似,并且继承自它,但在除以零等情况下也会表现出不同的行为。

NumPy 布尔值是另一个常见的混淆来源。Python 程序员有时会编写 obj is True,并且当显示为 True 的对象无法通过测试时会感到惊讶。当值显示为 np.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()

一个例外是具有文档和尤其是文档测试的下游库。由于许多值的表示形式将发生变化,在许多情况下,文档将需要更新。预计这将在中期需要更大的文档修复。

可能需要采用 doctest 测试工具来允许对新表示形式进行近似值检查。

arr.tofile() 的更改#

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

该提案是将默认值(回退)更改为使用 str 而不是 repr。如果需要 repr,用户将需要传递 fmt=%r

详细描述#

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

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

  • np.scalar(<value>),即 np.float64(3.0) 用于所有数值 dtypes。

  • np.longdouble('3.0') 用于 np.longdoublenp.clongdouble。这确保它始终可以正确循环往复,并且与 decimal.Decimal 的行为方式匹配。对于这两个,基于大小的名称,例如 float128 将不会使用,因为实际大小取决于平台,因此具有误导性。

  • np.str_("string")np.bytes_(b"byte_string") 用于字符串 dtypes。

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

与数组不同,标量表示形式应能够正确循环往复,因此 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),当数组的 dtype 不匹配时。对于 longdouble(具有匹配的 dtype),它将打印为 fill_value='3.1',包括引号,这(原则上,但可能在实践中并非如此)确保了循环往复。需要注意的是,对于字符串,字符串长度通常不匹配,因此字符串通常会打印为 np.str_("N/A")

np.record 标量将与 np.void 保持一致,并与之完全相同(除了名称本身)。例如,如下所示:np.record((3, 5), dtype=[('a', '

有关 longdoubleclongdouble 的详细信息#

对于 longdoubleclongdouble 值,例如

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

除非将它们作为字符串引用,否则可能无法循环往复(因为转换为 Python 浮点数会丢失精度)。本 NEP 建议使用与 Python 的 decimal 相同的单引号,它打印为 Decimal('3.0')

longdouble 可以具有不同的精度和存储大小,范围从 8 到 16 字节不等。但是,即使 float128 是正确的,因为数字以 128 位存储,但通常没有 128 位精度。(clongdouble 也是如此,但存储大小是其两倍。)

因此,本 NEP 包括更改 longdouble 的名称以始终打印为 longdouble,而不是 float128float96 的建议。它不包括弃用 np.float128 别名的建议。但是,这样的弃用可能会独立于 NEP 发生。

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

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

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

该提案将导致 longlong 类型名称,同时使用 int64 形式的标量。这种选择是由于 int64 通常对用户更有用,但类型名称本身必须准确。

实现#

注意

这部分尚未初始 PR 中实现。需要类似的更改来修复打印中的某些情况,并允许例如包含 longdouble 的结构化标量进行完全正确的打印。将来也可能需要类似的解决方案来允许自定义 DTypes 正确打印。

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

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() 将接受 reprstr(不是字符串的单例)来格式化不带类型信息('3.1' 而不是 np.longdouble('3.1'))的元素。实现确保格式化匹配,除了类型信息。

    空的格式字符串将与 str()(可能在传递数据时添加额外的填充)完全相同。

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

使 get_formatter 公开允许将其用于 np.record 和掩码数组。目前,格式化程序本身似乎是半公共的;使用单个入口点将希望为格式化 NumPy 值提供一个清晰的 API。

标量表示形式的很大一部分更改以前由 Ganesh Kathiresan 在 [2] 中完成。

替代方案#

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

对于布尔值,一个替代方案是使用 np.bool_(True)bool_(True)。但是,NumPy 布尔标量是单例,建议的格式化更简洁。在 [1] 中也讨论了布尔值的替代方案。

对于字符串标量,混淆通常不那么严重。延迟更改这些可能是合理的。

非有限值#

该提案不允许复制粘贴 naninf 值。它们可以用 np.float64('nan')np.float64(np.nan) 表示。这更简洁,Python 也使用 naninf,而不是允许通过显示为 float('nan') 进行复制粘贴。可以说,这在 NumPy 中是一个较小的补充,在那里它们将始终打印。

新的 get_formatter() 的替代方案#

当传递 fmt= 时,特别是对于本 NEP 中的主要用途(格式化为 reprstr)。也可以使用 ufunc 或直接格式化函数,而不是将其包装到 `get_formatter()` 中,该函数依赖于为 DType 实例化格式化程序类。

本 NEP 不排除创建 ufunc 或创建特殊路径。但是,NumPy 数组格式化通常会查看所有要格式化的值,以便为对齐添加填充或提供统一的指数输出。在这种情况下,传递 data= 并用于准备。这种格式化方式(与标量情况不同,其中 data=None 会是期望的)不幸的是与 UFuncs 本质上不兼容。

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

讨论#

参考文献和脚注#