通用函数 (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 方法#

所有ufuncs都有四种方法.它们可以在 方法 找到.然而,这些方法仅对接受两个输入参数并返回一个输出参数的标量ufuncs有意义.尝试在其他ufuncs上调用这些方法将导致 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_`(或 :class:`numpy.uint)数据类型.在前面的例子中:

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

最后,*out* 关键字允许你提供一个输出数组(或者对于多输出 ufuncs 提供一个输出数组的元组).如果给出了 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])

Ufuncs 也有第五种方法,:func: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.

所有ufuncs也可以接受输出参数,这些参数必须是数组或子类.如果需要,结果将被转换为提供的输出数组的数据类型.如果输出具有 __array_wrap__ 方法,则调用该方法而不是在输入中找到的方法.

广播#

参见

广播基础

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

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

类型转换规则#

备注

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

每个 ufunc 的核心是一个一维的跨步循环,它为特定的类型组合实现实际的函数.当创建一个 ufunc 时,它会得到一个内部循环的静态列表和一个相应的类型签名列表,ufunc 在这些类型上操作.ufunc 机制使用这个列表来确定在特定情况下使用哪个内部循环.你可以检查特定 ufunc 的 .types 属性,以查看哪些类型组合有定义的内部循环以及它们产生的输出类型(为了简洁,在输出中使用了 字符代码).

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

备注

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’ 类型不能被 ufuncs 操作.另外,请注意在 32 位系统上,整数类型可能有不同的大小,导致表略有不同.

混合标量-数组操作使用了一套不同的转换规则,确保标量不能”向上转换”数组,除非标量是与数组根本不同类型的数据(即,在数据类型层次结构中处于不同的层次).这一规则使你可以在代码中使用标量常量(作为Python类型,在ufuncs中相应地解释),而不必担心标量常量的精度是否会导致对大型(小精度)数组的向上转换.

使用内部缓冲区#

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

错误处理#

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

覆盖 ufunc 行为#

类(包括 ndarray 子类)可以通过定义某些特殊方法来重写 ufuncs 对它们的行为.详情请参见 标准数组子类.