PAYATHON 2026

为什么 np.vectorize 包装函数后会改变 uint8 溢出结果?

数据小陈

结论

题中结果取决于传给 f 的对象类型。otypes=[np.float64] 指定的是输出数组的类型,不会在调用 f 之前把输入转成浮点数。

两次 np.vectorize 调用的输入容器也不同:第一次传入 uint8 类型的 ndarray,第二次传入包含 np.uint8 标量的 Python 列表。两种输入转换成 object 数组后,逐元素调用 f 时拿到的对象类型可能不同。

下面按题中观察到的输出解释。原问题没有提供 NumPy 版本,标量类型提升和警告行为可能因版本而异。

为什么直接调用得到 216?

函数先计算 x - 80,再乘以 C:

def f(x):
    return C * (x - 80)

当 x 是题中的 uint8 数组时,减法在该类型下发生下溢。uint8 的取值范围是 0~255,结果按模 256 回绕:

40 - 80 = -40
-40 mod 256 = 216

随后乘以 1.0,得到浮点数 216.0。这时转换成浮点数已经无法恢复原本的负数结果。

为什么 np.vectorize 得到负数,却仍然警告溢出?

未指定 otypes 时,np.vectorize 默认会用输入的第一个元素调用函数,以推断输出类型。指定 otypes 可以省略这次探测调用。NumPy 文档

在题述情形中,cache=False 是默认设置。这里要区分对 f 的两次调用:

  1. 类型探测调用:从 uint8 数组取出第一个元素,得到 np.uint8(40)。在产生题述输出的环境中,减法回绕并发出警告,函数返回 216.0,np.vectorize 据此确定输出类型。
  2. 实际逐元素调用:普通调用路径先将输入转为 object 数组。题中的 uint8 数组转换后包含 Python int 对象,所以传给 f 的是整数 40。计算 40 - 80 得到 -40,再乘以 1.0,得到 -40.0。

这两个阶段使用的标量类型不同,因此可能既出现溢出警告,最终结果又是 [-40.]。实际计算中的 Python int 来自输入转换,输出类型推断不会将它转换为 np.float64。相关转换顺序可见 NumPy 的 vectorize 实现。

为什么指定 otypes 后又得到 216?

对比两次调用的输入:

np.vectorize(f)(np.array([np.uint8(40)]))
np.vectorize(f, otypes=[np.float64])([np.uint8(40)])

第二次直接传入列表。列表转换成 object 数组时,可以保留其中已有的 np.uint8 标量;而数值型 ndarray 转换成 object 数组时,会把这里的元素转换成 Python 整数。

可以在当前环境中检查这种差别:

import numpy as np

array_input = np.array([np.uint8(40)])
list_input = [np.uint8(40)]

print(np.__version__)
print(type(np.asarray(array_input, dtype=object)[0]))
print(type(np.asarray(list_input, dtype=object)[0]))

因此,题中第二次调用虽然跳过了类型探测,实际计算时接收到的仍是 np.uint8(40),减法仍可能回绕为 216。结果随后保存为 np.float64,得到 [216.]。

要单独比较 otypes 的作用,应保持输入不变:

a = np.array([np.uint8(40)])

print(np.vectorize(f)(a))
print(np.vectorize(f, otypes=[np.float64])(a))

在题述行为对应的实现和默认参数下,两者最终都应得到 [-40.];显式指定 otypes 的调用跳过了会产生警告的探测步骤。

怎样稳定地得到正确结果?

在减法之前显式选择合适的计算类型。对于这个简单的数组表达式,可以直接使用 NumPy 运算:

import numpy as np

C = 1.0
a = np.array([np.uint8(40)])

result = C * (a.astype(np.float64) - 80)
print(result)  # [-40.]

如果需要保留一个兼容数组和列表的函数,可以在函数入口转换:

def f_safe(x):
    x = np.asarray(x, dtype=np.float64)
    return C * (x - 80)

print(f_safe(np.array([np.uint8(40)])))  # [-40.]
print(f_safe([np.uint8(40)]))           # [-40.]

转换必须发生在减法之前。(a - 80).astype(np.float64) 会先产生回绕结果,再转换类型,无法解决问题。

np.vectorize 主要用于方便地逐元素调用 Python 函数,其实现本质上是循环;这类简单数组计算直接使用 NumPy 表达式即可。NumPy 文档

备注:内容仅供参考。