通用函数( ufunc )基础#

通用函数(简称 ufunc )是一个以元素方式对 ndarrays 进行操作的函数,支持 array broadcasting , type casting 和其他几个标准特性.也就是说,ufunc 是一个 “vectorized “ 包装器,用于接受固定数量的特定输入并产生固定数量的特定输出的函数.

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

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

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

Ufunc 方法#

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

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

>>> 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 关键字允许您更改进行 reduce 操作的数据类型(因此也是输出的类型).因此,您可以确保输出是一种精度足够高的数据类型来处理您的输出.更改 reduce 类型的责任主要由您承担.有一种例外情况:如果没有为“add”或“multiply”操作的 reduce 提供 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 ,它允许使用高级索引执行原地操作.在使用高级索引的维度上不使用 buffering ,因此高级索引可以多次列出一个项目,并且该操作将在该项目先前操作的结果上执行.

输出类型确定#

如果 ufunc(或其方法)的输入参数是 ndarrays ,那么输出也将是.例外情况是当结果是零维时,在这种情况下,输出将被转换为 array scalar .可以通过传入 out=...out=Ellipsis 来避免这种情况.

如果部分或全部输入参数不是 ndarrays ,那么输出可能也不是 ndarray .实际上,如果任何输入定义了 __array_ufunc__ 方法,控制权将完全传递给该函数,即 ufunc 将被 overridden .

如果没有输入覆盖 ufunc,那么所有输出数组将被传递给输入的 __array_wrap__ 方法(除了 ndarrays 和标量),该输入定义了 __array_priority__ 并且该值高于通用函数的任何其他输入.ndarray 的默认 __array_priority__ 是 0.0,子类的默认 __array_priority__ 是 0.0.矩阵的 __array_priority__ 等于 10.0.

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

广播#

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

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

类型转换规则#

备注

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

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

每当 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 对它们的作用方式.有关详细信息,请参见 标准数组子类 .