通用函数 (ufunc) 基础#

另请参阅

通用函数 (ufunc)

通用函数(或简称 ufunc)是在 ndarrays 上以逐元素方式运行的函数,支持 数组广播类型转换 和其他几个标准功能。也就是说,ufunc 是对接受固定数量特定输入并产生固定数量特定输出的函数的“矢量化”包装。

在 NumPy 中,通用函数是 numpy.ufunc 类别的实例。许多内置函数在编译后的 C 代码中实现。基本 ufunc 在标量上运行,但也有一种通用类型,其基本元素是子数组(向量、矩阵等),广播在其他维度上进行。最简单的例子是加法运算符

>>> np.array([0,2,3,4]) + np.array([1,1,-1,2])
array([1, 3, 2, 6])

还可以使用 numpy.frompyfunc 工厂函数生成自定义 numpy.ufunc 实例。

Ufunc 方法#

所有 ufunc 都有四种方法。它们可以在 方法 中找到。但是,这些方法仅对接受两个输入参数并返回一个输出参数的标量 ufunc 有意义。尝试在其他 ufunc 上调用这些方法会导致 ValueError

类似于 reduce 的方法都接受 axis 关键字、dtype 关键字和 out 关键字,并且所有数组都必须具有维度 >= 1。axis 关键字指定数组将要执行减少操作的轴(负值从后向前计数)。通常,它是一个整数,但对于 numpy.ufunc.reduce,它也可以是 int 的元组,以同时减少多个轴,或 None,以减少所有轴。例如

>>> x = np.arange(9).reshape(3,3)
>>> x
array([[0, 1, 2],
      [3, 4, 5],
      [6, 7, 8]])
>>> np.add.reduce(x, 1)
array([ 3, 12, 21])
>>> np.add.reduce(x, (0, 1))
36

dtype 关键字允许您管理在使用 ufunc.reduce 时产生的一个非常常见的问题。有时您可能有一个特定数据类型的数组,并希望将所有元素加起来,但结果不适合数组的数据类型。这通常发生在您有一个单字节整数数组时。dtype 关键字允许您更改进行减少操作的数据类型(以及输出的类型)。因此,您可以确保输出的数据类型具有足够大的精度来处理您的输出。更改减少类型的大部分责任由您负责。有一个例外:如果对“add”或“multiply”操作进行减少时未给出 dtype,则如果输入类型是整数(或布尔值)数据类型,并且小于 numpy.int_ 数据类型的尺寸,它将在内部向上转换为 int_(或 numpy.uint)数据类型。在前面的例子中

>>> x.dtype
dtype('int64')
>>> np.multiply.reduce(x, dtype=float)
array([ 0., 28., 80.])

最后,out 关键字允许您提供一个输出数组(或对于多输出 ufunc 的输出数组元组)。如果给出 out,则 dtype 参数仅用于内部计算。考虑前一个例子中的 x

>>> y = np.zeros(3, dtype=int)
>>> y
array([0, 0, 0])
>>> np.multiply.reduce(x, dtype=float, out=y)
array([ 0, 28, 80])

Ufunc 还具有第五种方法,numpy.ufunc.at,它允许使用高级索引执行就地操作。在使用高级索引的维度上不会使用任何 缓冲,因此高级索引可以多次列出一个项目,并且操作将在该项目之前操作的结果上执行。

输出类型确定#

ufunc(及其方法)的输出不一定是 ndarray,如果所有输入参数都不是 ndarrays。实际上,如果任何输入定义了 __array_ufunc__ 方法,则会将控制权完全传递给该函数,即 ufunc 被 重写

如果没有任何输入覆盖 ufunc,则所有输出数组将被传递到定义它的输入(除了 ndarrays 和标量)的 __array_wrap__ 方法,并且具有任何其他输入到通用函数的最高 __array_priority__。ndarray 的默认 __array_priority__ 为 0.0,子类型的默认 __array_priority__ 为 0.0。矩阵的 __array_priority__ 等于 10.0。

所有 ufunc 还可以接受必须是数组或子类的输出参数。如有必要,结果将被转换为提供的输出数组的数据类型。如果输出具有 __array_wrap__ 方法,则会调用该方法,而不是调用在输入上找到的方法。

广播#

另请参阅

广播基础

每个通用函数都接受数组输入并生成数组输出,通过在输入上以逐元素方式执行核心函数(其中元素通常是标量,但对于通用 ufunc 可以是向量或更高阶的子数组)。应用标准 广播规则,以便不完全共享相同形状的输入仍然可以有效地进行操作。

根据这些规则,如果输入在其形状中的某个维度大小为 1,则该维度中的第一个数据条目将用于沿该维度的所有计算。换句话说,ufunc 的步进机制将不会沿该维度进行步进(该维度的 步幅 将为 0)。

类型转换规则#

注意

在 NumPy 1.6.0 中,创建了一个类型提升 API 来封装确定输出类型的机制。有关更多详细信息,请参见函数 numpy.result_typenumpy.promote_typesnumpy.min_scalar_type

每个 ufunc 的核心是一个一维带步进循环,它为特定类型组合实现了实际函数。创建 ufunc 时,会为其提供一个静态的内部循环列表和一个相应的类型签名列表,ufunc 在其上运行。ufunc 机制使用此列表来确定对特定情况使用哪个内部循环。您可以检查特定 ufunc 的 .types 属性,以查看哪些类型组合具有定义的内部循环,以及它们产生什么输出类型(为简洁起见,在所述输出中使用 字符代码)。

每当 ufunc 没有为提供的输入类型提供核心循环实现时,都需要对一个或多个输入进行转换。如果找不到输入类型的实现,则算法会搜索具有类型签名的实现,所有输入都可以“安全地”转换为该类型签名。它在其内部循环列表中找到的第一个将被选中并执行,在所有必要的类型转换之后。请记住,ufunc 期间的内部副本(即使是转换)也仅限于内部缓冲区的大小(它是用户可设置的)。

注意

NumPy 中的通用函数足够灵活,可以具有混合类型签名。因此,例如,可以定义一个通用函数,它可以与浮点值和整数值一起使用。请参见 numpy.ldexp 以了解示例。

根据上述描述,转换规则本质上是通过一个问题来实现的,即何时可以将一种数据类型“安全地”转换为另一种数据类型。可以使用以下函数调用在 Python 中确定此问题的答案:can_cast(fromtype, totype)。以下示例显示了作者 64 位系统上对 24 种内部支持类型进行此调用后的结果。您可以使用示例中给出的代码为您的系统生成此表。

示例

显示 64 位系统“可以安全地转换”表的代码段。通常,输出取决于系统;您的系统可能导致不同的表。

>>> mark = {False: ' -', True: ' Y'}
>>> def print_table(ntypes):
...     print('X ' + ' '.join(ntypes))
...     for row in ntypes:
...         print(row, end='')
...         for col in ntypes:
...             print(mark[np.can_cast(row, col)], end='')
...         print()
...
>>> print_table(np.typecodes['All'])
X ? b h i l q n p B H I L Q N P e f d g F D G S U V O M m
? Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y - Y
b - Y Y Y Y Y Y Y - - - - - - - Y Y Y Y Y Y Y Y Y Y Y - Y
h - - Y Y Y Y Y Y - - - - - - - - Y Y Y Y Y Y Y Y Y Y - Y
i - - - Y Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
l - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
q - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
n - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
p - - - - Y Y Y Y - - - - - - - - - Y Y - Y Y Y Y Y Y - Y
B - - Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y - Y
H - - - Y Y Y Y Y - Y Y Y Y Y Y - Y Y Y Y Y Y Y Y Y Y - Y
I - - - - Y Y Y Y - - Y Y Y Y Y - - Y Y - Y Y Y Y Y Y - Y
L - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
Q - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
N - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
P - - - - - - - - - - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - -
e - - - - - - - - - - - - - - - Y Y Y Y Y Y Y Y Y Y Y - -
f - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y Y Y Y - -
d - - - - - - - - - - - - - - - - - Y Y - Y Y Y Y Y Y - -
g - - - - - - - - - - - - - - - - - - Y - - Y Y Y Y Y - -
F - - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y - -
D - - - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y - -
G - - - - - - - - - - - - - - - - - - - - - Y Y Y Y Y - -
S - - - - - - - - - - - - - - - - - - - - - - Y Y Y Y - -
U - - - - - - - - - - - - - - - - - - - - - - - Y Y Y - -
V - - - - - - - - - - - - - - - - - - - - - - - - Y Y - -
O - - - - - - - - - - - - - - - - - - - - - - - - - Y - -
M - - - - - - - - - - - - - - - - - - - - - - - - Y Y Y -
m - - - - - - - - - - - - - - - - - - - - - - - - Y Y - Y

您应该注意,虽然为了完整性而包含在表中,但‘S’、‘U’ 和‘V’ 类型不能由 ufunc 操作。此外,请注意,在 32 位系统上,整数类型的大小可能不同,导致表略有改变。

混合标量-数组操作使用一组不同的转换规则,这些规则确保标量不能“向上转换”数组,除非标量是与数组根本不同的数据类型(即,在数据类型层次结构中的不同层次结构下)。此规则使您能够在代码中使用标量常量(它们作为 Python 类型在 ufunc 中被相应地解释),而无需担心标量常量的精度会导致对大型(低精度)数组进行向上转换。

使用内部缓冲区#

在内部,缓冲区用于未对齐数据、交换数据以及必须从一种数据类型转换为另一种数据类型的数据。内部缓冲区的大小可以在每个线程的基础上进行设置。最多可以创建 \(2 (n_{\mathrm{inputs}} + n_{\mathrm{outputs}})\) 个指定大小的缓冲区来处理来自 ufunc 的所有输入和输出的数据。缓冲区的默认大小为 10,000 个元素。每当需要基于缓冲区的计算,但所有输入数组都小于缓冲区大小,则在计算进行之前,这些行为异常或类型不正确的数组将被复制。因此,调整缓冲区的大小可能会改变各种类型的 ufunc 计算完成的速度。使用函数 numpy.setbufsize 可以访问用于设置此变量的简单接口。

错误处理#

通用函数可能会触发硬件中的特殊浮点状态寄存器(例如除以零)。如果您的平台上可用,这些寄存器将在计算过程中定期检查。错误处理在每个线程的基础上控制,可以使用函数 numpy.seterrnumpy.seterrcall 配置。

重写 ufunc 行为#

类(包括 ndarray 子类)可以通过定义某些特殊方法来覆盖 ufunc 对它们的处理方式。有关详细信息,请参阅 标准数组子类