文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。
通用函数
另请参阅 通用函数(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 都有四个方法。这些方法可以在 Methods 中找到。然而,这些方法仅适用于接受两个输入参数并返回一个输出参数的标量 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 关键字允许你改变归并操作的数据类型(以及因此输出的数据类型)。因此,你可以确保输出是一个精度足够大的数据类型来处理你的输出。改变归并类型的责任主要由你承担。有一个例外:如果在“加”或“乘”操作的归并中没有给出 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])
Ufuncs 还有一个第五种方法,numpy.ufunc.at
,它允许使用高级索引进行原地操作。在使用高级索引的维度上不会使用缓冲区,因此高级索引可以多次列出同一个项,并且该操作将在该项的前一个操作的结果上执行。
输出类型确定
Ufunc(及其方法)的输出不一定是 ndarray
,如果所有输入参数都不是 ndarrays
。实际上,如果任何输入定义了 __array_ufunc__
方法,则将完全将控制权移交给该函数,即 ufunc 被覆盖。
如果没有任何输入覆盖了 ufunc,则所有输出数组将传递给输入的 __array_wrap__
方法(除了 ndarrays
和标量之外)定义它 并且 具有比其他输入到通用函数更高的 __array_priority__
。ndarray
的默认 __array_priority__
是 0.0,子类型的默认 __array_priority__
也是 0.0。矩阵的 __array_priority__
等于 10.0。
所有 ufunc 也可以接受输出参数,这些参数必须是数组或子类。如果需要,结果将被强制转换为提供的输出数组(或子类)的数据类型。
如果输出具有 __array_wrap__
方法,则将调用它,而不是在输入中找到的那个。
广播(Broadcasting)
另请参阅 广播基础知识
每个通用函数(ufunc)都接受数组输入,并通过对输入进行逐元素操作来产生数组输出(这里的元素通常是一个标量,但对于泛化的 ufunc,它可以是一个向量或更高阶的子数组)。标准的广播规则会被应用,以便即使输入的形状不完全相同,仍然可以有效地进行操作。
根据这些规则,如果输入数组在某个维度上的大小为 1,那么该维度的第一个数据元素将被用于该维度上的所有计算。换句话说,ufunc 的步进机制将不会沿着该维度进行步进(该维度的步长将为 0)。
类型转换规则
每个 ufunc 的核心是一个一维的步进循环,该循环为特定的类型组合实现了实际的函数。当创建 ufunc 时,会为其提供一个静态的内部循环列表和一个对应的类型签名列表,ufunc 机制使用这个列表来确定特定情况下使用哪个内部循环。你可以通过检查特定 ufunc 的 .types
属性来查看哪些类型组合有定义的内部循环以及它们产生哪种输出类型(出于简洁性,使用字符代码表示输出类型)。
当 ufunc 没有为提供的输入类型提供核心循环实现时,必须对一个或多个输入进行类型转换。如果找不到输入类型的实现,则算法会搜索一个所有输入都可以“安全地”转换到的类型签名的实现。它会在其内部循环列表中找到的第一个这样的实现被选中并执行,在执行所有必要的类型转换之后。请注意,ufunc 中的内部拷贝(即使是类型转换时)也受到内部缓冲区大小(用户可设置)的限制。
注意
NumPy 中的通用函数足够灵活,可以拥有混合类型签名。因此,例如,可以定义一个通用函数,它既适用于浮点数又适用于整数值。请参见 numpy.ldexp
以获取示例。
根据上述描述,类型转换规则本质上是由何时可以将一种数据类型“安全地”转换为另一种数据类型来决定的。可以通过函数调用 can_cast(fromtype, totype)
在 Python 中确定此问题的答案。下面的示例显示了作者的 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
i
n
p
u
t
s
+
n
o
u
t
p
u
t
s
)
2 (n_{\mathrm{inputs}} + n_{\mathrm{outputs}})
2(ninputs+noutputs) 个指定大小的缓冲区,用于处理 ufunc 的所有输入和输出数据。默认的缓冲区大小是 10,000 个元素。每当基于缓冲区的计算需要进行时,但如果所有输入数组的大小都小于缓冲区大小,那么这些行为不端或类型不正确的数组将在计算开始前被复制。因此,调整缓冲区的大小可能会改变各种 ufunc 计算完成的速度。一个简单的接口可以通过函数 numpy.setbufsize
来设置这个变量。
错误处理
通用函数可能会触发硬件中的特殊浮点状态寄存器(例如除以零)。如果在你的平台上可用,这些寄存器将在计算过程中定期检查。错误处理是按线程进行控制的,可以使用函数 numpy.seterr
和 numpy.seterrcall
进行配置。
覆盖 ufunc 行为
类(包括 ndarray
子类)可以通过定义某些特殊方法来覆盖 ufunc 对它们的操作方式。具体细节,请参见 标准数组子类。
风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。