狂热主义包括忘记目标时加倍努力。— 乔治·桑塔亚娜
权威是一个可以告诉您更多事情的人,而您并不是真正想知道的事情。— 未知
本章试图解释一些新代码背后的逻辑。这些解释的目的是使人们能够比仅仅盯着代码更容易地理解实现的思想。也许以此方式,可以使更多的人改进,借鉴和/或优化算法。
ndarray的一个基本方面是,将数组视为从某个位置开始的内存“块”。此内存的解释取决于步幅信息。对于- 维数组中的每个维,整数(步幅)指示必须跳过多少个字节才能到达该维中的下一个元素。除非您具有单段数组,否则在遍历数组时必须参考此步幅信息。编写接受跨步的代码并不难,您只需使用(char *)指针,因为跨步以字节为单位。还请记住,步幅不必是元素大小的单位倍数。另外,请记住,如果数组的维数为0(有时称为rank-0数组),则步幅和维度变量为NULL。
除了的跨步和维度成员中包含的结构信息之外PyArrayObject
,这些标志还包含有关如何访问数据的重要信息。特别是,NPY_ARRAY_ALIGNED
根据数据类型数组,当存储器位于适当的边界时,将设置标志。即使您有连续的内存块,也不能仅仅假设取消对元素的数据类型特定的指针的引用是安全的。仅当
NPY_ARRAY_ALIGNED
设置了此标志后,此操作才是安全的操作(在某些平台上可以运行,但在其他平台上(例如Solaris),它将导致总线错误)。的NPY_ARRAY_WRITEABLE
,如果你计划写入阵列的存储区域也应得到保证。也有可能获得指向不可写存储区的指针。有时,当NPY_ARRAY_WRITEABLE
没有设置标志只会很粗鲁。有时它可能导致程序崩溃(例如
,数据区域是只读的内存映射文件)。
数据类型是ndarray的重要抽象。操作将寻找数据类型,以提供对阵列进行操作所需的关键功能。该功能在PyArray_Descr
结构的'f'成员所指向的函数指针列表中提供
。这样,通过PyArray_Descr
在'f'成员中提供具有适当功能指针的结构,可以简单地扩展数据类型的数量。对于内置类型,有一些绕过此机制的优化,但是数据类型抽象的要点是允许添加新的数据类型。
void数据类型是内置数据类型之一,它允许包含1个或多个字段作为数组元素的任意结构化类型。字段只是另一个数据类型对象,以及到当前结构化类型的偏移量。为了支持任意嵌套的字段,为void类型实现了几种数据类型访问的递归实现。一个常见的习惯用法是循环浏览字典的元素,并基于以给定偏移量存储的数据类型对象执行特定操作。这些偏移量可以是任意数字。因此,必须识别遇到错位数据的可能性,并在必要时予以考虑。
在许多NumPy代码中,一个非常常见的操作是需要遍历一般的,跨步的N维数组的所有元素。通用N维循环的此操作从迭代器对象的概念中抽象出来。要编写N维循环,只需从ndarray创建迭代器对象,使用迭代器对象结构的dataptr成员,然后调用迭代器对象上的宏
PyArray_ITER_NEXT
(it)即可移至下一个元素。“下一个”元素始终按C连续顺序排列。宏的工作原理是:首先对C连续,1-D和2-D情况进行特殊封装,使其非常简单。
对于一般情况,迭代通过跟踪迭代器对象中的坐标计数器列表来进行。在每次迭代中,最后一个坐标计数器都会增加(从0开始)。如果此计数器比该维度上的数组大小小一个(预先计算并存储的值),则该计数器增加,而dataptr成员增加该维度上的跨度,宏结束。如果到达维度的末尾,则将最后一个维度的计数器重置为零,并且通过将步幅值乘以该维度中元素数的乘积乘以一个步长值,将dataptr移回到该维度的开始处(这是还预先计算并存储在迭代器对象的backstrides成员中)。在这种情况下,宏不会结束,但是减少了本地维度计数器,以便倒数第二个维度代替了上一个维度所扮演的角色,并且在倒数第二个维度上再次执行了上述测试。以这种方式,针对任意步幅适当地调整了dataptr。
PyArrayIterObject
除非基础数组是C连续的,否则结构的坐标成员将保持当前的Nd计数器,在这种情况下,绕过坐标计数。索引成员PyArrayIterObject
保持跟踪迭代器的当前平面索引。它由PyArray_ITER_NEXT
宏更新。
在Numpy的祖先Numeric中,广播是通过深埋在ufuncobject.c中的几行代码实现的。在NumPy中,广播的概念已被抽象化,因此可以在多个地方进行广播。广播由功能处理PyArray_Broadcast
。此函数需要PyArrayMultiIterObject
传入一个(或等效于二进制的东西)。PyArrayMultiIterObject
跟踪广播的维数和每个维中的大小以及广播结果的总大小。它还跟踪正在广播的阵列的数量,以及每个正在广播的阵列的指向迭代器的指针。
该PyArray_Broadcast
函数采用已经定义的迭代器,并使用它们来确定每个维度上的广播形状(在广播发生的同时创建迭代器,然后使用该PyMultiIter_New
函数)。然后,调整迭代器,以使每个迭代器都认为它正在广播大小的数组上进行迭代。这可以通过调整迭代器的维数以及每个维中的形状来完成。这是可行的,因为迭代器的步幅也已调整。广播仅调整(或添加)长度为1的尺寸。对于这些维度,将strides变量简单地设置为0,以便当广播操作在扩展维度上进行操作时,该数组上迭代器的数据指针不会移动。
广播始终使用数值0的跨度进行扩展。在NumPy中,它的完成方式完全相同。最大的区别是,现在在a中跟踪了跨步数组,在a中跟踪PyArrayIterObject
了广播结果中涉及的迭代器PyArrayMultiIterObject
,并且该PyArray_BroadCast
调用实现了广播规则。
数组标量提供了Python类型的层次结构,该层次结构允许数组中存储的数据类型与从数组中提取元素时返回的Python类型之间一一对应。对象数组对此规则进行了例外处理。对象数组是任意Python对象的异构集合。当从对象数组中选择一个项目时,您将获得原始的Python对象(而不是存在但实际上很少用于实际的对象数组标量)。
数组标量还提供与数组相同的方法和属性,目的是可以使用相同的代码来支持任意维(包括0维)。数组标量是只读的(不可变的),除了void标量也可以写入,使得结构化数组字段设置更自然地工作(a [0] ['f1'] = value
)。
arr[index]
通过首先准备索引并找到索引类型来组织所有python索引操作。支持的索引类型为:
整数
新轴
切片
省略
整数数组/类似数组(花式)
布尔(单个布尔数组); 如果有多个布尔数组作为索引,或者形状不完全匹配,则布尔数组将转换为整数数组。
0-d布尔值(也是整数);0维布尔数组是一种特殊情况,必须在高级索引代码中进行处理。它们表示必须将0维布尔数组解释为整数数组。
与标量数组的特殊情况一样,它表示将整数数组解释为整数索引,这很重要,因为整数数组索引会强制执行复制,但是如果返回标量(完整整数索引),则会被忽略。除了超出范围的值和高级索引的广播错误外,准备好的索引保证有效。这包括为不完整的索引添加省略号,例如,当使用单个整数对二维数组进行索引时。
下一步取决于找到的索引类型。如果所有维都用整数索引,则返回或设置标量。单个布尔索引数组将调用专用的布尔函数。包含省略号或切片但没有高级索引的索引将始终通过计算新的步幅和内存偏移来创建旧数组的视图。然后可以返回此视图,也可以使用填充该视图以进行分配PyArray_CopyObject
。请注意,
当数组为对象dtype时,也可以在其他分支的临时数组上调用PyArray_CopyObject以支持复杂的分配。
到目前为止,最复杂的情况是高级索引编制,它可以与典型的基于视图的索引编制结合使用,也可以不结合使用。在这里,整数索引被解释为基于视图的。在尝试理解这一点之前,您可能需要使自己熟悉它的微妙之处。高级索引代码具有三个不同的分支和一个特殊情况:
有一个索引数组,它以及赋值数组都可以被简单地迭代。例如,它们可以是连续的。同样,索引数组必须是intp
类型,赋值中的值数组应该是正确的类型。这纯粹是一条捷径。
只有整数数组索引,因此不存在子数组。
基于视图的索引和高级索引混合在一起。在这种情况下,基于视图的索引定义了由高级索引组合的子数组的集合。例如,通过垂直堆叠的子阵列创建的,和
。arr[[1, 2, 3], :]
arr[1, :]
arr[2,:]
arr[3, :]
有一个子数组,但是它只有一个元素。这种情况可以像没有子数组一样处理,但是在安装过程中需要注意。
确定适用的情况,检查广播以及确定所需的转置类型都在PyArray_MapIterNew中完成。设置后,有两种情况。如果没有子数组或只有一个元素,则不需要子数组迭代,并准备一个迭代器,该迭代器将迭代所有索引数组以及结果或值数组。如果有子数组,则准备三个迭代器。一种用于索引数组,一种用于结果或值数组(减去其子数组),另一种用于原始数组和结果/赋值数组的子数组。前两个迭代器将指针分配(或允许计算)到子数组的开头,然后允许重新启动子数组迭代。
当高级索引彼此相邻时,可能需要进行转置。PyArray_MapIterSwapAxes
除非要求PyArray_MapIterNew分配结果,否则所有必要的转置均由调用方处理,并且必须由调用方处理。
在准备之后,尽管需要考虑不同的迭代模式,但获取和设置相对简单。除非在获取项目期间只有一个索引数组,否则将预先检查索引的有效性。否则,将在内部循环中对其进行处理以进行优化。
通用函数是可调用的对象,它们通过将基本的1-D循环包装成一个完整的易于使用的函数来获取输入并产生
输出,这些函数可以无缝地实现广播,类型检查和缓冲强制以及输出参数处理。尽管有一种从Python函数(
frompyfunc
)创建ufunc的机制,但新的通用函数通常是用C创建的。用户必须提供一维循环,以实现基本功能,该功能采用输入标量值并将生成的标量放入实现中说明的适当输出槽中。
每个ufunc计算都涉及与设置计算有关的一些开销。这种开销的实际意义是,即使ufunc的实际计算速度非常快,您仍可以编写数组和特定于类型的代码,这些代码对于小型数组的工作比ufunc更快。特别是,使用ufuncs在0-D数组上执行许多计算将比其他基于Python的解决方案要慢(无声导入的scalarmath模块的存在正是为了使数组标量具有基于ufunc的计算外观,并显着减少了开销)。
调用ufunc时,必须完成许多事情。从这些设置操作中收集的信息存储在循环对象中。该循环对象是C结构(可以成为Python对象,但由于仅在内部使用而未进行初始化)。此循环对象具有需要与PyArray_Broadcast一起使用的布局,以便可以按照与其他代码部分相同的方式来处理广播。
首先要做的是在特定于线程的全局词典中查找缓冲区大小,错误掩码和关联的错误对象的当前值。错误掩码的状态控制发现错误情况时将发生的情况。应当注意,仅在每个1-D循环执行之后才执行硬件错误标志的检查。这意味着,如果输入和输出数组是连续的且具有正确的类型,以便执行单个一维循环,则只有在计算完数组的所有元素之后,才能检查标志。在特定于线程的字典中查找这些值需要花费时间,对于非常小的数组,所有其他值都容易忽略这些时间。
在检查了特定于线程的全局变量之后,对输入进行求值以确定ufunc应该如何进行,并在必要时构造输入和输出数组。非数组的所有输入都将转换为数组(必要时使用上下文)。注意哪个输入是标量(因此转换为0-D数组)。
接下来,根据输入数组类型,从ufunc可用的1-D循环中选择适当的1-D循环。通过尝试将输入数据类型的签名与可用签名进行匹配来选择此一维循环。与内置类型相对应的签名存储在ufunc结构的type成员中。与用户定义类型相对应的签名存储在功能信息的链接列表中,头元素存储为
CObject
在userloops词典中,以数据类型编号为键(参数列表中的第一个用户定义类型用作键)。搜索签名,直到找到一个可以将所有输入数组安全地转换为签名的签名(忽略任何不允许确定结果类型的标量参数)。此搜索过程的含义是,在存储签名时,应将“较小类型”放在“较大类型”下面。如果未找到一维循环,则报告错误。否则,将使用存储的签名更新arguments_list -如果需要强制转换并修复1-D循环假定的输出类型。
如果ufunc有2个输入和1个输出,而第二个输入是Object数组,则执行特殊情况检查,以便在第二个输入不是ndarray,具有__array_priority__属性并且具有__r {op的情况下,返回NotImplemented } __特殊方法。通过这种方式,将向Python发出信号,让其他对象有机会完成操作,而不是使用通用对象数组计算。例如,这允许稀疏矩阵覆盖乘法运算符的一维循环。
对于小于指定缓冲区大小的输入数组,将复制所有不连续,未对齐或字节外的数组,以确保对于小数组使用单个循环。然后,为所有输入数组创建数组迭代器,并将生成的迭代器集合广播为单个形状。
然后处理输出参数(如果有),并构造所有丢失的返回数组。如果提供的任何输出数组的类型不正确(或未对齐)并且小于缓冲区大小,则使用特殊WRITEBACKIFCOPY
标志集构造一个新的输出数组
。在函数的末尾,将调用,
PyArray_ResolveWritebackIfCopy
以便将其内容复制回输出数组。然后处理输出参数的迭代器。
最后,决定如何执行循环机制,以确保将输入数组的所有元素组合在一起以生成正确类型的输出数组。循环执行的选项是单循环(用于连续,对齐和正确的数据类型),跨步循环(用于非连续但仍然对齐和正确的数据类型)和缓冲循环(用于未对齐或不正确的数据)类型情况)。根据调用哪种执行方法,然后建立并计算循环。
本节描述了如何为三种不同类型的执行中的每种执行建立和执行基本的通用函数计算循环。如果
NPY_ALLOW_THREADS
在编译期间定义了,则只要不涉及对象数组,就在调用循环之前释放Python全局解释器锁(GIL)。如有必要,可以重新获取它以处理错误情况。仅在完成一维循环后才检查硬件错误标志。
这是最简单的情况。通过仅一次调用基础的1-D循环来执行ufunc。仅当我们为输入和输出对齐了正确类型(包括字节顺序)的数据并且所有数组具有一致的步幅(连续,0-D或1-D)时,才有可能。在这种情况下,将调用一维计算循环一次以计算整个阵列的计算。注意,只有在整个计算完成后才检查硬件错误标志。
当输入和输出数组对齐并具有正确的类型,但步幅不均匀(不连续且2D或更大)时,则采用第二个循环结构进行计算。此方法将输入和输出参数的所有迭代器转换为对除最大维之外的所有维进行迭代。然后,内部循环由基础的一维计算循环处理。外循环是转换后的迭代器上的标准迭代器循环。每个1-D循环完成后,将检查硬件错误标志。
只要输入和/或输出数组未对齐或数据类型(包括字节交换)与基础一维循环的预期不符,该代码便可以处理这种情况。数组也被假定为非连续的。该代码的工作方式与strided循环非常相似,只是修改了内部1-D循环,以便对输入执行预处理,并对输出执行bufsize块(其中bufsize是用户可设置的)参数)。在复制的数据上调用基础的一维计算循环(如果需要)。在这种情况下,设置代码和循环代码要复杂得多,因为它必须处理:
临时缓冲区的内存分配
决定是否在输入和输出数据上使用缓冲区(未对齐和/或错误的数据类型)
为任何需要缓冲区的输入或输出复制并可能转换数据。
特殊外壳的对象数组,以便在需要复制和/或强制转换时可以正确处理引用计数。
将内部一维循环分解为bufsize块(可能还有余数)。
同样,在每个1-D循环的末尾检查硬件错误标志。
Ufunc允许其他类似数组的类通过接口无缝传递,因为特定类的输入将导致输出属于同一类。其工作机制如下。如果任何输入不是ndarrays并定义了
__array_wrap__
方法,则具有最大__array_priority__
属性的类
将确定所有输出的类型(传入的任何输出数组除外)。__array_wrap__
输入数组的
方法将以ufunc返回的ndarray作为输入来调用。有两种调用样式__array_wrap__
功能支持。第一个将ndarray作为第一个参数,将“ context”的元组作为第二个参数。上下文是(ufunc,自变量,输出自变量编号)。这是第一次尝试通话。如果发生TypeError,则仅使用ndarray作为第一个参数来调用该函数。
与通用ufunc相似,有三种需要计算的ufunc方法。这些是减少,累积和减少。这些方法中的每一个都需要一个setup命令和一个循环。与无元素,单元素,跨步循环和缓冲循环相对应的方法有四种循环样式。除了无元素和单元素情况(当输入数组对象分别具有0和1元素时发生的特殊情况)外,这些与用于通用函数调用的基本循环样式相同。
这三种方法的设置功能均为construct_reduce
。此函数创建一个减少循环的对象,并用完成循环所需的参数填充它。所有这些方法仅适用于具有2个输入并返回1个输出的ufunc。因此,底层1-d环选择假定的签名[ otype
,
otype
,otype
]其中otype
是请求的减少量数据类型。然后从(每个线程)全局存储中检索缓冲区大小和错误处理。对于未对齐或数据类型不正确的小型阵列,将进行复制,以便使用未缓冲的代码段。然后,选择循环策略。如果数组中有1个元素或0个元素,则选择一种简单的循环方法。如果该数组未未对齐且具有正确的数据类型,则选择跨步循环。否则,必须执行缓冲循环。然后建立循环参数,并构造返回数组。输出数组的形状不同,具体取决于方法是缩小,累加还是缩小。如果已经提供了输出数组,则检查其形状。如果输出数组不是C连续的,则对齐,并且具有正确的数据类型,然后使用WRITEBACKIFCOPY标志设置的临时副本。这样,这些方法将能够使用行为良好的输出数组,但是在以下情况下,结果将被复制回真实的输出数组:PyArray_ResolveWritebackIfCopy
在函数完成时调用。最后,将迭代器设置为在正确的轴上循环(取决于提供给该方法的轴的值),并且设置例程将返回到实际的计算例程。
所有ufunc方法都使用相同的底层一维计算循环,并调整了输入和输出参数,以便进行适当的归约。例如,reduce功能的关键是调用一维循环,并且输出和第二个输入指向内存中的相同位置,并且步长均为0。第一个输入指向输出。输入数组,其步长由所选轴的适当步幅给出。这样,执行的操作是
其中是输入中元素的数量
,
是输出,
是沿所选轴的
元素
。对尺寸大于1的数组重复此基本操作,以使沿选定轴的每个1-D子数组都进行缩减。删除了选定维的迭代器将处理此循环。
对于缓冲循环,在调用循环函数之前必须小心复制和强制转换数据,因为基础循环期望对齐的数据具有正确的数据类型(包括字节顺序)。在不大于用户指定的bufsize的块上调用循环函数之前,缓冲的循环必须处理此复制和转换。
累加函数与归约函数非常相似,因为输出和第二个输入都指向输出。不同之处在于第二个输入指向内存的位置比当前输出指针大了一个。因此,执行的操作是
输出具有与输入相同的形状,并且当选定轴上的形状为时,每个1-D循环对元素进行操作
。同样,缓冲循环在调用基础的一维计算循环之前要小心复制和转换数据。
reduceat函数是reduce和累加函数两者的概括。它实现了减少由索引指定的输入数组的范围。在进行循环计算之前,请检查extraindex参数以确保每个输入对于沿选定维度的输入数组而言都不会太大。使用与减少代码非常相似的代码处理循环实现,重复代码的次数与索引输入中存在元素的次数相同。特别是:传递到基础1-D计算循环的第一个输入指针在索引数组指示的正确位置指向输入数组。此外,传递到基础1-D循环的输出指针和第二个输入指针指向内存中的同一位置。一维计算循环的大小固定为当前索引与下一个索引之间的差(当当前索引为最后一个索引时,则假定下一个索引为数组沿选定维度的长度)。这样,一维循环将对指定的索引执行缩减。
使用缓冲的代码来处理未对齐的数据或与输入和/或输出数据类型不匹配的循环数据类型,其中在调用之前将输入数据复制到临时缓冲区中,并在需要时强制转换为正确的数据类型基本的一维函数。临时缓冲区的创建(元素)大小不大于用户可设置的缓冲区大小值。因此,该循环必须足够灵活,以足以调用基础的一维计算循环足够多次,以不大于缓冲区大小的块形式完成总计算。