通用函数(ufunc)基础知识
导包
import numpy as np
通用函数(或简称 ufunc)是以逐个元素的方式对 ndarrays 进行操作的函数,支持数组广播、类型转换和其他几个标准功能。也就是说,ufunc 是一个函数的“矢量化”包装器,它接受固定数量的特定输入并产生固定数量的特定输出。
在 NumPy 中,通用函数是 numpy.ufunc 类的实例。许多内置函数都是在编译的 C 代码中实现的。基本 ufuncs 在标量上运行,但也有一个广义类型,其基本元素是子数组(向量、矩阵等),广播在其他维度上完成。最简单的例子是加法运算符:
print(np.array([0, 2, 3, 4]) + np.array([1, 1, -1, 2]))
[1 3 2 6]
还可以使用 numpy.frompyfunc 工厂函数生成自定义 numpy.ufunc 实例。
【1】Ufunc方法
所有 ufuncs 都有四种方法。可以在方法中找到它们。但是,这些方法仅在接受两个输入参数并返回一个输出参数的标量 ufunc 上有意义。尝试在其他 ufuncs 上调用这些方法将导致 ValueError.
类似reduce的方法都采用轴关键字,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 关键字允许您更改发生缩减的数据类型(以及输出的类型)。因此,您可以确保输出是精度足以处理输出的数据类型。更改reduce类型的责任主要取决于您。有一个例外:如果没有给出 dtype 来减少“加法”或“乘法”运算,那么如果输入类型是整数(或布尔)数据类型并且小于 numpy.int__ 数据类型的大小,它将在内部向上转换为 int_(或 numpy.uint)数据类型。
在前面的示例中:
x.dtype
dtype('int32')
np.multiply.reduce(x, dtype=float)
array([ 0., 28., 80.])
最后,out 关键字允许您提供一个输出数组(对于单输出 ufuncs,这是目前唯一支持的;但是,对于将来的扩展,可以传入具有单个参数的元组)。如果给出 out,则忽略 dtype 参数。考虑上一个示例中的 x:
y = np.zeros(3, dtype=int)
print(y)
[0 0 0]
np.multiply.reduce(x, dtype=float, out=y)
array([ 0, 28, 80])
Ufuncs 还有第五种方法 numpy.ufunc.at,它允许使用高级索引执行就地操作。对使用高级索引的维度不使用缓冲,因此高级索引可以多次列出一个项目,并且将对该项目的上一个操作的结果执行该操作。
【2】输出类型确定
ufunc(及其方法)的输出不一定是ndarray,如果所有输入参数都不是ndarrayndarrays。实际上,如果任何输入定义了__array_ufunc__方法,则控制权将完全传递给该函数,即ufunc被覆盖。
如果没有输入覆盖 ufunc,则所有输出数组都将传递给定义它的输入的__array_prepare__和__array_wrap__方法(ndarrays 和标量),并且具有通用函数的任何其他输入的最高__array_priority__。ndarray 的默认__array_priority__为 0.0,子类型的默认__array_priority__为 0.0。__array_priority__矩阵的__array_priority__等于 10.0。
所有 ufuncs 也可以接受输出参数。如有必要,输出将被强制转换为提供的输出数组的数据类型。如果使用具有 array 方法的类进行输出,则结果将写入 array 返回的对象。然后,如果类也有__array_prepare__方法,则调用它,以便可以根据 ufunc 的上下文(由 ufunc 本身、传递给 ufunc 的参数和 ufunc 域组成的上下文)确定元数据。__array_prepare__返回的数组对象被传递给 ufunc 进行计算。最后,如果类也有 array_wrap 方法,则返回的 ndarray 结果将在将控制权传递回调用方之前传递给该方法。
【3】广播
每个通用函数接受数组输入并通过对输入逐个执行核心函数来生成数组输出(其中元素通常是标量,但可以是广义 ufunc 的向量或高阶子数组)。应用标准广播规则,以便仍然可以有效地操作不共享完全相同形状的输入。
根据这些规则,如果输入的形状中的维度大小为 1,则该维度中的第一个数据条目将用于沿该维度进行的所有计算。换句话说,ufunc 的步进机制根本不会沿着该维度步进(该维度的步幅将为 0)。
【4】类型转换规则
每个 ufunc 的核心是一个一维跨步循环,它实现了特定类型组合的实际功能。创建 ufunc 时,会为其提供内部循环的静态列表和 ufunc 操作的相应类型签名列表。ufunc 机器使用此列表来确定在特定情况下使用哪个内部循环。您可以检查特定 ufunc 的 .types 属性,以查看哪些类型组合具有定义的内部循环以及它们生成的输出类型(为简洁起见,在所述输出中使用了字符代码)。
每当 ufunc 没有提供的输入类型的核心循环实现时,必须在一个或多个输入上完成转换。如果找不到输入类型的实现,则算法将搜索具有类型签名的实现,所有输入都可以“安全地”强制转换为该实现。在所有必要的类型转换之后,选择并执行它在其内部循环列表中找到的第一个循环。回想一下,ufuncs 期间的内部副本(即使是强制转换)仅限于内部缓冲区的大小(用户可设置)。
通过上面的描述,强制转换规则本质上是通过何时可以将一种数据类型“安全地”强制转换为另一种数据类型的问题来实现的。这个问题的答案可以在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 p B H I L Q 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
b - 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
i - - - 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
p - - - - - 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
H - - - 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
L - - - - - Y Y - - Y Y Y Y - - Y Y - Y Y Y Y Y Y - Y
Q - - - - - - - - - - - Y Y - - Y Y - Y Y Y Y Y Y - -
P - - - - - - - - - - - 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 Y Y - -
F - - - - - - - - - - - - - - - - - Y Y Y Y Y Y Y - -
D - - - - - - - - - - - - - - - - - - Y Y Y Y Y Y - -
G - - - - - - - - - - - - - - - - - - Y 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
注意,虽然为了完整性而包含在表中,但 ufuncs 不能对 ‘S’、‘U’ 和 ‘V’ 类型进行操作。另请注意,在 32 位系统上,整数类型可能具有不同的大小,从而导致表略有更改。
混合标量数组操作使用一组不同的强制转换规则,这些规则确保标量不能“向上转换”数组,除非标量是与数组根本不同类型的数据(即,在数据类型层次结构中的不同层次结构下)。此规则使您能够在代码中使用标量常量(作为 Python 类型,在 ufuncs 中相应地解释),而不必担心标量常量的精度是否会导致大(小精度)数组的上行转换。
【5】使用内部缓冲器
在内部,缓冲区用于未对齐的数据、交换的数据以及必须从一种数据类型转换为另一种数据类型的数据。内部缓冲区的大小可基于每个线程进行设置。最多可以有
2 ( n inputs + n outputs ) 2\left(n_{\text {inputs }}+n_{\text {outputs }}\right) 2(ninputs +noutputs )
为处理来自 ufunc 的所有输入和输出的数据而创建的指定大小的缓冲区。缓冲区的默认大小为 10,000 个元素。每当需要基于缓冲区的计算,但所有输入数组都小于缓冲区大小时,将在计算继续之前复制这些行为异常或类型不正确的数组。因此,调整缓冲区的大小可能会改变完成各种 ufunc 计算的速度。可以使用函数numpy.setbufsize访问用于设置此变量的简单界面.
【6】错误处理
通用函数可能会触发硬件中的特殊浮点状态寄存器(例如被零除)。如果您的平台上可用,将在计算过程中定期检查这些寄存器。错误处理基于每个线程进行控制,可以使用函数numpy.seterr和numpy.seterrcall进行配置numpy.seterr.
【7】覆盖ufunc行为
类(包括 ndarray 子类)可以通过定义某些特殊方法来覆盖 ufuncs 对它们的作用方式。