通用函数 (ufunc
) 基础#
另请参阅
通用函数(简称 ufunc)是一个以逐元素方式操作 ndarrays
的函数,支持数组广播、类型转换和其他几个标准功能。也就是说,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 的方法都接受一个 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
,它允许使用高级索引执行原地操作。在使用高级索引的维度上不使用 缓冲,因此高级索引可以多次列出一个项,并且该操作将在该项的先前操作的结果上执行。
输出类型确定#
如果所有输入参数不是 ndarray
,则 ufunc(及其方法)的输出不一定是 ndarray
。实际上,如果任何输入定义了 __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_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 对它们的操作方式。有关详细信息,请参见标准数组子类。