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(结构化 dtype)

此外,其余 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 Notebook 单元格中的文档和输出通常会完整显示类型信息。

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)

  • np.longdoublenp.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 值将用引号括起来,其他值永远不会被截断。

在某些地方(例如掩码数组、空值和记录标量),我们希望打印不带类型的表示形式。例如

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

应打印带引号的 3.0(以确保往返转换),但不要重复完整的 np.longdouble('3.0'),因为数据类型包含 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 也是一样,但存储大小是两倍。)

因此,本 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 的结构化标量。将来也预计需要类似的解决方案来允许自定义数据类型正确打印。

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

针对空值标量和掩码 fill_value 提出的更改使得必须公开不带类型的标量表示形式。

我们建议引入半公开 API

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

以替换当前的内部函数 _get_formatting_func。与旧函数相比,这将允许两件事

  • data 可以为 None(如果传递了 dtype),从而允许不传递多个稍后将打印/格式化的值。

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

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

get_formatter() 预计将来会查询用户数据类型的方法,允许对所有数据类型进行自定义格式化。

使 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() 中。

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

使用单例 strrepr 确保将来像 f"{arr:r}" 这样的格式字符串不会受到使用 "r""s" 而不是 reprstr 的任何限制。

讨论#

参考文献和脚注#