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.uint8, np.int8,以及所有其他整数标量

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

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

在某些地方(例如掩码数组、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 将被调整为仅包含完整的标量信息,例如当数组的 dtype 不匹配时使用 fill_value=np.float64(1e20)。对于 longdouble(dtype 匹配),它将打印为 fill_value='3.1'(包含引号),这原则上(但在实践中可能不会)确保了往返。需要注意的是,对于字符串,dtype 的字符串长度不匹配是很常见的。因此,字符串通常将打印为 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,它打印为 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 的结构化标量。将来也可能需要类似的解决方案来允许自定义 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() 将接受 reprstr(单例而不是字符串)来格式化不包含类型信息的元素('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)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"的限制。

讨论#

参考文献和脚注#