NumPy 中的数据类型提升#
混合使用两种不同数据类型时,NumPy 必须确定操作结果的适当 dtype。此步骤称为 *提升* 或 *查找公共 dtype*。
在典型情况下,用户无需担心提升的细节,因为提升步骤通常可以确保结果要么与输入匹配,要么超过输入的精度。
例如,当输入具有相同的 dtype 时,结果的 dtype 与输入的 dtype 匹配。
>>> np.int8(1) + np.int8(1)
np.int8(2)
混合使用两种不同的 dtype 通常会产生具有较高精度输入 dtype 的结果。
>>> np.int8(4) + np.int64(8) # 64 > 8
np.int64(12)
>>> np.float32(3) + np.float16(3) # 32 > 16
np.float32(6.0)
在典型情况下,这不会导致意外情况。但是,如果您使用非默认 dtype(例如无符号整数和低精度浮点数),或者混合使用 NumPy 整数、NumPy 浮点数和 Python 标量,则 NumPy 提升规则的一些细节可能与相关。请注意,这些详细规则并不总是与其他语言的规则相匹配 [1]。
数值 dtype 分为四种“类型”,具有自然的层次结构。
无符号整数 (
uint
)有符号整数 (
int
)浮点数 (
float
)复数 (
complex
)
除了类型之外,NumPy 数值 dtype 还具有相关的精度(以位为单位)。类型和精度共同指定了 dtype。例如,uint8
是使用 8 位存储的无符号整数。
操作的结果将始终等于或高于任何输入的类型。此外,结果的精度将始终大于或等于输入的精度。仅此一项,就可能导致一些意想不到的例子。
混合使用浮点数和整数时,整数的精度可能会强制结果转换为更高精度的浮点数。例如,涉及
int64
和float16
的操作的结果是float64
。混合使用具有相同精度的无符号整数和有符号整数时,结果的精度将 *高于* 任何输入。此外,如果其中一个已经具有 64 位精度,则没有更高精度的整数可用,例如,涉及
int64
和uint64
的操作会得到float64
。
请参阅下面的 数值提升 部分和图像以了解详细信息。
Python 标量的详细行为#
自 NumPy 2.0 [2] 以来,我们的提升规则中的一个重要点是,虽然涉及两个 NumPy dtype 的操作永远不会丢失精度,但涉及 NumPy dtype 和 Python 标量 (int
、float
或 complex
) 的操作 *可能* 会丢失精度。例如,Python 整数和 NumPy 整数之间的操作结果应该是 NumPy 整数,这可能是直观的。但是,Python 整数具有任意精度,而所有 NumPy dtype 都有固定精度,因此无法保留 Python 整数的任意精度。
更一般地说,NumPy 会考虑 Python 标量的“类型”,但在确定结果 dtype 时会忽略它们的精度。这通常很方便。例如,当使用低精度 dtype 的数组时,通常希望 Python 标量的简单操作能够保留 dtype。
>>> arr_float32 = np.array([1, 2.5, 2.1], dtype="float32")
>>> arr_float32 + 10.0 # undesirable to promote to float64
array([11. , 12.5, 12.1], dtype=float32)
>>> arr_int16 = np.array([3, 5, 7], dtype="int16")
>>> arr_int16 + 10 # undesirable to promote to int64
array([13, 15, 17], dtype=int16)
在这两种情况下,结果精度都由 NumPy dtype 决定。因此,arr_float32 + 3.0
的行为与 arr_float32 + np.float32(3.0)
相同,而 arr_int16 + 10
的行为与 arr_int16 + np.int16(10.)
相同。
另一个例子是,当混合使用 NumPy 整数和 Python float
或 complex
时,结果类型始终为 float64
或 complex128
。
>> np.int16(1) + 1.0 np.float64(2.0)
但是,当使用低精度 dtype 时,这些规则也可能导致意外行为。
首先,由于 Python 值在操作执行之前会转换为 NumPy 值,因此当结果看起来很明显时,操作可能会因错误而失败。例如,np.int8(1) + 1000
无法继续,因为 1000
超过了 int8
的最大值。当 Python 标量无法强制转换为 NumPy dtype 时,会引发错误。
>>> np.int8(1) + 1000
Traceback (most recent call last):
...
OverflowError: Python integer 1000 out of bounds for int8
>>> np.int64(1) * 10**100
Traceback (most recent call last):
...
OverflowError: Python int too large to convert to C long
>>> np.float32(1) + 1e300
np.float32(inf)
... RuntimeWarning: overflow encountered in cast
其次,由于始终忽略 Python 浮点数或整数精度,因此低精度 NumPy 标量将继续使用其较低的精度,除非显式将其转换为较高精度 NumPy dtype 或 Python 标量(例如,通过 int()
、float()
或 scalar.item()
)。这种较低的精度可能会对某些计算有害或导致错误的结果,尤其是在整数溢出的情况下。
>>> np.int8(100) + 100 # the result exceeds the capacity of int8
np.int8(-56)
... RuntimeWarning: overflow encountered in scalar add
请注意,当标量发生溢出时,NumPy 会发出警告,但数组不会;例如,np.array(100, dtype="uint8") + 100
不会发出警告。
数值提升#
下图显示了数值提升规则,其中垂直轴为类型,水平轴为精度。
具有较高类型的输入 dtype 决定结果 dtype 的类型。结果 dtype 的精度尽可能低,但不会出现在图表中任一输入 dtype 的左侧。
请注意以下具体规则和观察结果:
当 Python
float
或complex
与 NumPy 整数交互时,结果将为float64
或complex128
(黄色边框)。NumPy 布尔值也将转换为默认整数 [3]。当另外涉及 NumPy 浮点数时,这一点不相关。精度图绘制方式使得
float16 < int16 < uint16
,因为大型uint16
不适合int16
,而大型int16
存储在float16
中时会损失精度。但是,由于 NumPy 始终认为float64
和complex128
是任何整数值的可接受提升结果,因此这种模式被打破了。一个特例是 NumPy 将许多有符号整数和无符号整数的组合提升为
float64
。这里使用了较高类型,因为没有有符号整数 dtype 足够精确以容纳uint64
。
一般提升规则的例外#
在 NumPy 中,提升是指特定函数对结果执行的操作,在某些情况下,这意味着 NumPy 可能会偏离 np.result_type 提供的结果。
sum
和 prod
的行为#
当对整数值(或布尔值)求和时,np.sum
和 np.prod
将始终返回默认整数类型。这通常是 int64
。这样做的原因是整数求和很容易溢出并给出令人困惑的结果。此规则也适用于底层的 np.add.reduce
和 np.multiply.reduce
。
NumPy 或 Python 整数标量的显著行为#
NumPy 的类型提升指的是结果 dtype 和操作精度,但操作有时会决定结果。除法始终返回浮点值,比较始终返回布尔值。
这导致了看起来像是规则“例外”的情况
NumPy 与 Python 整数或混合精度整数的比较始终返回正确的结果。输入永远不会以丢失精度的方式进行转换。
无法提升类型的相等比较将被视为全部为
False
(相等)或全部为True
(不相等)。像
np.sin
这样的始终返回浮点值的单目数学函数,通过将其转换为float64
来接受任何 Python 整数输入。除法始终返回浮点值,因此也允许通过将 NumPy 整数和 Python 整数值都转换为
float64
来进行两者之间的除法。
原则上,对于其他函数,其中一些例外可能是有意义的。如果您认为情况如此,请提出问题。
非数值数据类型的提升#
NumPy 将提升扩展到非数值类型,尽管在许多情况下提升没有明确定义,并且 simply rejected。
适用以下规则:
NumPy 字节字符串(
np.bytes_
)可以提升为 Unicode 字符串(np.str_
)。但是,对于非 ASCII 字符,将字节转换为 Unicode 将失败。出于某些目的,NumPy 将几乎所有其他数据类型提升为字符串。这适用于数组创建或连接。
当没有可行的提升时,像
np.array()
这样的数组构造函数将使用object
dtype。当结构化 dtype 的字段名称和顺序匹配时,可以提升结构化 dtype。在这种情况下,所有字段都会分别提升。
NumPy
timedelta
在某些情况下可以与整数提升。
注意
其中一些规则有些令人意外,并且正在考虑在将来进行更改。但是,任何向后不兼容的更改都必须权衡破坏现有代码的风险。如果您对提升应该如何工作有特别的建议,请提出问题。
提升的 dtype
实例的详细信息#
上述讨论主要涉及混合不同 DType 类时的行为。附加到数组的 dtype
实例可以携带其他信息,例如字节序、元数据、字符串长度或精确的结构化 dtype 布局。
虽然结构化 dtype 的字符串长度或字段名称很重要,但 NumPy 将字节序、元数据和结构化 dtype 的精确布局视为存储细节。在提升过程中,NumPy *不会*考虑这些存储细节:* 字节序将转换为本地字节序。* 附加到 dtype 的元数据可能会或可能不会保留。* 生成的结构化 dtype 将被打包(但如果输入已对齐则对齐)。
对于大多数程序来说,这种行为是最佳行为,在这些程序中,存储细节与最终结果无关,并且使用不正确的字节序可能会大大降低评估速度。