NEP 5 — 通用函数的泛化#
- 状态:
最终
正如 https://scipy.org.cn/scipy/numpy/wiki/GeneralLoopingFunctions 上所解释的那样,普遍需要不仅对标量上的函数进行循环,而且对向量(或数组)上的函数进行循环。我们建议通过泛化通用函数 (ufunc) 来实现这个概念,并提供一个 C 实现,该实现向 numpy 代码库添加了约 500 行代码。在当前(专门的)ufunc 中,基本函数仅限于元素级操作,而泛化版本支持“子数组”级“子数组”操作。Perl 向量库 PDL 提供了类似的功能,其术语在以下内容中被重新使用。
每个泛化的 ufunc 都与之关联的信息,表明输入的“核心”维数以及相应的输出维数(元素级 ufunc 的核心维数为零)。所有参数的核心维数列表称为 ufunc 的“签名”。例如,ufunc numpy.add 的签名为 (),()->()
,定义了两个标量输入和一个标量输出。
另一个示例是(参见 GeneralLoopingFunctions 页面)函数 inner1d(a,b)
,其签名为 (i),(i)->()
。它沿着每个输入的最后一个轴应用内积,但保留其余索引。例如,其中 a
的形状为 (3,5,N)
,而 b
的形状为 (5,N)
,这将返回形状为 (3,5)
的输出。基础基本函数被调用 3*5 次。在签名中,我们为每个输入指定一个核心维数 (i)
,为输出指定零个核心维数 ()
,因为它接受两个 1-d 数组并返回一个标量。通过使用相同的名称 i
,我们指定这两个对应的维数应该具有相同的大小(或其中一个大小为 1 且将被广播)。
核心维数之外的维数称为“循环”维数。在上面的示例中,这对应于 (3,5)
。
通常的 numpy“广播”规则适用,其中签名决定如何将每个输入/输出对象的维数拆分为核心和循环维数。
当输入数组的维数小于相应核心维数的数量时,将在其形状前添加 1。
从所有输入中删除核心维数,剩余的维数将被广播;定义循环维数。
输出由循环维数加上输出核心维数给出。
定义#
- 基本函数
每个 ufunc 都包含一个基本函数,该函数对数组参数的最小部分执行最基本的操作(例如,将两个数字相加是将两个数组相加的最基本操作)。ufunc 对数组的不同部分多次应用基本函数。基本函数的输入/输出可以是向量;例如,inner1d 的基本函数以两个向量作为输入。
- 签名
签名是一个字符串,描述了 ufunc 基本函数的输入/输出维数。有关更多详细信息,请参见下面的部分。
- 核心维数
基本函数的每个输入/输出的维数由其核心维数定义(零个核心维数对应于标量输入/输出)。核心维数映射到输入/输出数组的最后维数。
- 维数名称
维数名称表示签名中的核心维数。不同的维数可以共享一个名称,表示它们具有相同的大小(或可广播的大小)。
- 维数索引
维数索引是一个整数,表示维数名称。它根据签名中每个名称首次出现的顺序枚举维数名称。
签名的详细信息#
签名定义了输入和输出变量的“核心”维数,从而也定义了维数的收缩。签名由以下格式的字符串表示。
每个输入或输出数组的核心维数由括号中的维数名称列表表示,
(i_1,...,i_N)
;标量输入/输出用()
表示。可以将任何有效的 Python 变量名用作i_1
、i_2
等。不同参数的维数列表用
","
分隔。输入/输出参数用"->"
分隔。如果在多个位置使用相同的维数名称,这将强制对应维数具有相同的大小(或可广播的大小)。
签名的正式语法如下
<Signature> ::= <Input arguments> "->" <Output arguments>
<Input arguments> ::= <Argument list>
<Output arguments> ::= <Argument list>
<Argument list> ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument> ::= "(" <Core dimension list> ")"
<Core dimension list> ::= nil | <Dimension name> |
<Dimension name> "," <Core dimension list>
<Dimension name> ::= valid Python variable name
说明
所有引号仅用于清楚起见。
共享相同名称的核心维数必须是可广播的,就像我们上面示例中的两个
i
一样。每个维数名称通常对应于基本函数实现中的一个循环级别。空格将被忽略。
以下是一些签名的示例
add |
|
|
inner1d |
|
|
sum1d |
|
|
dot2d |
|
矩阵乘法 |
outer_inner |
|
沿着最后一个维数进行内积,沿着倒数第二个维数进行外积,沿着其余维数进行循环/广播。 |
用于实现基本函数的 C-API#
当前接口保持不变,并且仍然可以使用 PyUFunc_FromFuncAndData
来实现(专门的)ufunc,该函数由标量基本函数组成。
可以使用 PyUFunc_FromFuncAndDataAndSignature
来声明更通用的 ufunc。参数列表与 PyUFunc_FromFuncAndData
相同,但有一个额外的参数用于指定签名作为 C 字符串。
此外,回调函数的类型与以前相同,void (*foo)(char **args, intp *dimensions, intp *steps, void *func)
。当调用时,args
是一个长度为 nargs
的列表,包含所有输入/输出参数的数据。对于标量基本函数,steps
的长度也为 nargs
,表示用于参数的步幅。dimensions
是一个指向单个整数的指针,该整数定义要循环的轴的大小。
对于非平凡的签名,dimensions
还将包含核心维数的大小,从第二个条目开始。每个唯一维数名称只提供一个大小,并且大小是根据签名中维数名称首次出现的顺序提供的。
前 nargs
个元素的 steps
与标量 ufunc 相同。后面的元素包含所有参数的所有核心维数的步幅,按顺序排列。
例如,考虑一个签名为 (i,j),(i)->()
的 ufunc。在这种情况下,args
将包含三个指向输入/输出数组 a
、b
、c
数据的指针。此外,dimensions
将是 [N, I, J]
,以定义循环的大小 N
以及核心维数 i
和 j
的大小 I
和 J
。最后,steps
将是 [a_N, b_N, c_N, a_i, a_j, b_i]
,包含所有必要的步幅。