通用函数 (ufunc) 基础#
另请参阅
通用函数(简称 ufunc)是一种以逐元素方式操作 ndarray 的函数,支持 数组广播、类型转换 和其他一些标准特性。也就是说,ufunc 是一个“矢量化”的函数包装器,它接受固定数量的特定输入并产生固定数量的特定输出。
在 NumPy 中,通用函数是 numpy.ufunc 类的实例。许多内置函数是用编译后的 C 代码实现的。基本的 ufunc 操作于标量,但也存在一种广义 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-like)的方法都接受一个 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 关键字参数允许您更改归约发生的数据类型(从而改变输出的类型)。因此,您可以确保输出的数据类型具有足够高的精度来处理您的输出。更改归约类型的责任主要在于您。有一个例外:如果对于“加法”或“乘法”运算的归约没有提供 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,那么输出也将是 ndarray。例外情况是当结果是零维的时,此时输出将被转换为一个数组标量(array scalar)。这可以通过传入 out=... 或 out=Ellipsis 来避免。
如果部分或全部输入参数不是 ndarray,那么输出也可能不是 ndarray。事实上,如果任何输入定义了 __array_ufunc__ 方法,控制权将完全转移到该函数,即 ufunc 被 覆盖。
如果没有任何输入覆盖 ufunc,那么所有输出数组都将被传递给定义了它的输入(除了 ndarray 和标量)中具有最高 __array_priority__ 的那个输入的 __array_wrap__ 方法。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_type、numpy.promote_types 和 numpy.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.seterr 和 numpy.seterrcall 进行配置。
覆盖 ufunc 行为#
类(包括 ndarray 的子类)可以通过定义特定的特殊方法来覆盖 ufunc 在它们上的行为。有关详细信息,请参阅 标准数组子类。