PEP 209 – 多维数组
- 作者:
- Paul Barrett <barrett at stsci.edu>, Travis Oliphant <oliphant at ee.byu.edu>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建日期:
- 2001年1月3日
- Python 版本:
- 2.2
- 发布历史:
摘要
本PEP提议对多维数组模块Numeric进行重新设计和重新实现,以使其更容易添加新特性和功能。Numeric 2将特别关注高效访问大小超过千兆字节且由非均匀数据结构或记录组成的数组。提议的设计使用四个Python类:ArrayType、UFunc、Array和ArrayView;以及一个低级C扩展模块_ufunc,以高效处理数组操作。此外,每种数组类型都有自己的C扩展模块,用于定义该类型的强制转换规则、操作和方法。这种设计使得可以模块化地添加新的类型、特性和功能。新版本将引入一些与当前Numeric不兼容之处。
动机
多维数组通常用于科学、工程和计算中存储和操作数据。Python目前有一个名为Numeric(此后称为Numeric 1)的扩展模块,它为操作中等大小(约10 MB)的同构数据数组的用户提供了令人满意的一组功能。对于访问可能非同构数据的更大数组(约100 MB或更多),Numeric 1的实现效率低下且繁琐。未来,随着PEP 211:向Python添加新的线性运算符和PEP 225:逐元素/逐对象运算符的说明,数值Python社区很可能要求额外的功能。
提案
本提案建议对Numeric 1进行重新设计和重新实现,此后称为Numeric 2,这将使得以简单和模块化的方式添加新的类型、特性和功能。Numeric 2的初始设计应侧重于提供一个通用框架来操作各种类型的数组,并应提供一个直接的机制来添加新的数组类型和UFuncs。更特定于不同学科的功能方法可以分层在此核心之上。这个新模块仍将被称为Numeric,并且Numeric 1中的大部分行为将得到保留。
提议的设计使用四个Python类:ArrayType、UFunc、Array和ArrayView;以及一个低级C扩展模块来高效处理数组操作。此外,每种数组类型都有自己的C扩展模块,用于定义该类型的强制转换规则、操作和方法。在核心功能稳定后,一些Python类可以在稍后转换为C扩展类型。
一些计划中的功能是
- 改进内存使用
此功能在处理大型数组时尤为重要,可以显著提高性能和内存使用。我们已经确定了几个可以改进内存使用的领域
- 使用局部强制转换模型
Numeric 2,像Numeric 1一样,将实现一个局部强制转换模型,而不是使用Python的全局强制转换模型(它创建临时数组),如PEP 208中所述,该模型将强制转换的责任推迟给运算符。通过使用内部缓冲区,可以在操作时(如果需要)对每个数组(包括输出数组)进行强制转换操作。基准测试[1]表明,性能最多只会略微下降,并且在内部缓冲区小于L2缓存大小且处理器处于负载状态时有所提高。为了完全避免数组强制转换,Numeric 2允许C函数具有混合类型的参数。
- 避免创建临时数组
在复杂的数组表达式中(即,具有多个操作),每个操作将创建一个临时数组,该数组将被后续操作使用然后删除。一个更好的方法是识别这些临时数组,并在可能的情况下重用它们的数据缓冲区,即当数组形状和类型与正在创建的临时数组相同时。这可以通过检查临时数组的引用计数来完成。如果它是1,那么它将在操作完成后被删除,并且是重用的候选者。
- 可选使用内存映射文件
Numeric用户有时需要访问来自非常大的文件的数据或处理大于可用内存的数据。内存映射数组提供了一种通过将数据存储在磁盘上同时使其看起来在内存中来做到这一点。内存映射数组应通过消除文件访问期间的两个复制步骤之一来改善所有文件的访问。Numeric应能够透明地访问内存中和内存映射数组。
- 记录访问
在某些科学领域,数据以二进制记录的形式存储在文件中。例如,在天文学中,光子数据以1维光子列表的形式按到达时间顺序存储。这些记录或类似C的结构包含有关检测到的光子的信息,例如其到达时间、其在探测器上的位置和其能量。每个字段可能具有不同类型,例如char、int或float。此类数组引入了必须处理的新问题,特别是可能需要执行字节对齐或字节交换才能正确访问数值(尽管字节交换也是内存映射数据的问题)。Numeric 2旨在在访问或操作数据时自动处理对齐和表示问题。实现记录有两种方法;作为派生数组类或特殊数组类型,具体取决于您的观点。我们将此讨论推迟到“开放问题”部分。
- 使用局部强制转换模型
- 附加数组类型
Numeric 1有11种定义类型:char、ubyte、sbyte、short、int、long、float、double、cfloat、cdouble和object。没有ushort、uint或ulong类型,也没有更复杂的类型,例如对某些科学领域有用且可能用于实现掩码数组的位类型。Numeric 1的设计使得添加这些和其他类型成为一个困难且容易出错的过程。为了能够轻松添加(和删除)新的数组类型(例如下面描述的位类型),有必要重新设计Numeric。
- 位类型
数组之间的丰富比较结果是一个布尔值数组。结果可以存储在char类型的数组中,但这是不必要的内存浪费。更好的实现将使用位或布尔类型,将数组大小压缩八倍。这目前正在为Numeric 1(由Travis Oliphant)实现,并应包含在Numeric 2中。
- 位类型
- 增强的数组索引语法
扩展切片语法已添加到Python中,通过允许大于1的步长来提供操作Numeric数组时更大的灵活性。此语法作为规则间隔索引列表的简写工作良好。对于需要不规则间隔索引列表的情况,增强的数组索引语法将允许将1D数组作为参数。
- 丰富的比较
在Python 2.1中实现PEP 207:丰富的比较,提供了操作数组时额外的灵活性。我们打算在Numeric 2中实现此功能。
- 数组广播规则
当标量与数组进行操作时,隐含的行为是创建一个与数组操作数形状相同的新数组,其中包含标量值。这称为数组广播。它也适用于较低维度的数组,例如向量。这种隐式行为在Numeric 1中实现,也将在Numeric 2中实现。
设计与实现
Numeric 2的设计有四个主要类
- ArrayType
这是一个简单的类,描述数组类型的基本属性,例如其名称、其以字节为单位的大小、其与其他类型的强制转换关系等,例如
Int32 = ArrayType('Int32', 4, 'doc-string')
其与其他类型的关系在该类型的C扩展模块导入时定义。相应的Python代码是
Int32.astype[Real64] = Real64
这表示Real64数组类型比Int32数组类型具有更高的优先级。
建议核心实现包含以下属性和方法。可以根据需要添加其他属性,例如位类型的.bitsize或.bitstrides。
属性
.name: e.g. "Int32", "Float64", etc. .typecode: e.g. 'i', 'f', etc. (for backward compatibility) .size (in bytes): e.g. 4, 8, etc. .array_rules (mapping): rules between array types .pyobj_rules (mapping): rules between array and python types .doc: documentation string
方法
__init__(): initialization __del__(): destruction __repr__(): representation
C-API:这仍需充实。
- UFunc
这个类是Numeric 2的核心。它的设计类似于ArrayType,即UFunc创建一个单例可调用对象,其属性包括名称、参数总数和输入参数数量、文档字符串以及一个空的CFunc字典;例如
add = UFunc('add', 3, 2, 'doc-string')
定义时,add实例没有与其关联的C函数,因此无法执行任何工作。CFunc字典在导入数组类型的C扩展模块时填充或注册。register方法的参数是:函数名、函数描述符和CUFunc对象。相应的Python代码是
add.register('add', (Int32, Int32, Int32), cfunc-add)
在数组类型模块的初始化函数中,例如Int32,有两个C API函数:一个用于初始化强制转换规则,另一个用于注册CFunc对象。
当一个操作应用于某些数组时,会调用
__call__方法。它获取每个数组的类型(如果未给定输出数组,则根据强制转换规则创建它),并检查 CFunc 字典中是否存在与参数类型匹配的键。如果存在,则立即执行操作,否则使用强制转换规则搜索相关操作和一组转换函数。__call__方法然后调用用 C 编写的计算方法来遍历每个数组的切片,即_ufunc.compute(slice, data, func, swap, conv)
“func”参数是一个CFuncObject,而“swap”和“conv”参数是需要预处理或后处理的数组的CFuncObjects列表,否则使用None。data参数是缓冲区对象的列表,slice参数给出每个维度的迭代次数以及每个数组和每个维度的缓冲区偏移量和步长。
我们已预定义了几个UFuncs供
__call__方法使用:cast、swap、getobj和setobj。cast和swap函数分别执行强制类型转换和字节交换,getobj和setobj函数在Numeric数组和Python序列之间执行强制类型转换。建议核心实现包含以下属性和方法。
属性
.name: e.g. "add", "subtract", etc. .nargs: number of total arguments .iargs: number of input arguments .cfuncs (mapping): the set C functions .doc: documentation string
方法
__init__(): initialization __del__(): destruction __repr__(): representation __call__(): look-up and dispatch method initrule(): initialize coercion rule uninitrule(): uninitialize coercion rule register(): register a CUFunc unregister(): unregister a CUFunc
C-API:这仍需充实。
- Array
此类别包含有关数组的信息,例如形状、类型、数据的字节序等。其运算符“+”、“-”,等等,仅调用相应的UFunc函数,例如
def __add__(self, other): return ufunc.add(self, other)
建议核心实现包含以下属性、方法和函数。
属性
.shape: shape of the array .format: type of the array .real (only complex): real part of a complex array .imag (only complex): imaginary part of a complex array
方法
__init__(): initialization __del__(): destruction __repr_(): representation __str__(): pretty representation __cmp__(): rich comparison __len__(): __getitem__(): __setitem__(): __getslice__(): __setslice__(): numeric methods: copy(): copy of array aslist(): create list from array asstring(): create string from array
函数
fromlist(): create array from sequence fromstring(): create array from string array(): create array with shape and value concat(): concatenate two arrays resize(): resize array
C-API:这仍需充实。
- ArrayView
这个类与Array类相似,只是reshape和flat方法会引发异常,因为非连续数组无法仅使用指针和步长信息进行重塑或展平。
C-API:这仍需充实。
- C扩展模块
Numeric2 将有几个 C 扩展模块。
- _ufunc
这组模块中的主要模块是_ufuncmodule.c。此模块的目的是执行最低限度的工作,即使用指定的C函数迭代数组。这些函数的接口与Numeric 1相同,即
int (*CFunc)(char *data, int *steps, int repeat, void *func);
并且它们的功能预计是相同的,即它们遍历最内层维度。
建议核心实现包含以下属性和方法。
属性
方法
compute():
C-API:这仍需充实。
- _int32, _real64, etc.
每个数组类型也将有C扩展模块,例如_int32module.c、_real64module.c等。如前所述,当UFunc模块导入这些模块时,它们将自动注册其函数和强制转换规则。这些模块的新版本或改进版本可以轻松实现和使用,而不会影响Numeric 2的其余部分。
- _ufunc
未解决的问题
- 切片语法默认是复制行为还是视图行为?
Python的默认行为是当使用切片语法时返回子列表或元组的副本,而Numeric 1返回数组的视图。Numeric 1的选择显然是出于性能原因:开发人员希望避免在每次数组操作期间分配和复制数据缓冲区的开销,并认为数组的深层复制需求很少见。然而,一些人认为Numeric的切片表示法也应该具有复制行为以与Python列表保持一致。在这种情况下,与复制行为相关的性能开销可以通过实现写时复制来最小化。这种方案使两个数组共享一个数据缓冲区(如视图行为),直到任一数组被分配新数据时才创建数据缓冲区的副本。然后,视图行为将由ArrayView类实现,其行为类似于Numeric 1数组,即非连续数组的.shape不可设置。ArrayView类的使用也明确了数组包含的数据类型。
- 项语法默认是复制行为还是视图行为?
一个类似的问题出现在项语法中。例如,如果
a = [[0,1,2], [3,4,5]]且b = a[0],那么改变b[0]也会改变a[0][0],因为a[0]是 a 的第一行的引用或视图。因此,如果 c 是一个 2D 数组,那么c[i]应该返回一个 1D 数组,它是 c 的视图而不是副本,以保持一致性。然而,c[i]可以被认为是c[i,:]的简写,这意味着如果切片语法返回一个副本,则意味着复制行为。Numeric 2 应该像列表一样返回视图还是应该返回副本? - 标量强制转换是如何实现的?
Python 的数字类型少于 Numeric,这可能导致强制转换问题。例如,当将 Python 的 float 类型的标量与 Numeric 的 float 类型的数组相乘时,Numeric 数组被转换为 double,因为 Python 的 float 类型实际上是 double。这通常不是期望的行为,因为 Numeric 数组的大小将增加一倍,这可能会令人烦恼,特别是对于非常大的数组。我们更喜欢对于相同的类型类(即整数、浮点数和复数),数组类型优先于 Python 类型。因此,Python 整数与 Int16(short)数组之间的操作将返回 Int16 数组。而 Python 浮点数与 Int16 数组之间的操作将返回 Float64(double)数组。两个数组之间的操作使用正常的强制转换规则。
- 整数除法如何处理?
在Python的未来版本中,整数除法的行为将发生变化。操作数将转换为浮点数,因此结果将是浮点数。如果我们在其中实现提议的标量强制转换规则(其中数组优先于Python标量),那么将数组除以整数将返回一个整数数组,这与Python的未来版本(将返回一个double类型的数组)不一致。科学程序员熟悉整数除法和浮点除法之间的区别,那么Numeric 2是否应该继续这种行为?
- 记录应该如何实现?
根据您的观点,实现记录有两种方法。第一种是根据其类型的行为将数组划分为不同的类。例如,数值数组是一个类,字符串是第二个,记录是第三个,因为每个类的操作范围和类型都不同。因此,记录数组不是一种新类型,而是一种更灵活的数组形式的机制。为了轻松访问和操作这种复杂数据,该类由具有不同字节偏移量进入数据缓冲区的数值数组组成。例如,可能有一个由Int16、Real32值数组组成的表。两个数值数组,一个偏移量为0字节,步长为6字节,解释为Int16;另一个偏移量为2字节,步长为6字节,解释为Real32,将表示记录数组。两个数值数组都将引用相同的数据缓冲区,但具有不同的偏移量和步长属性,以及不同的数值类型。
第二种方法是将记录视为众多数组类型中的一种,尽管其数组操作可能比数值数组少,甚至可能不同。这种方法将数组类型视为固定长度字符串的映射。该映射可以是简单的,如整数和浮点数,也可以是复杂的,如复数、字节字符串和C结构体。记录类型有效地将struct和Numeric模块合并成一个多维结构体数组。这种方法意味着数组接口的某些更改。例如,'typecode'关键字参数可能应该改为更具描述性的'format'关键字。
- 记录语义如何定义和实现?
无论采用哪种记录实现方法,如果希望访问记录的子字段,都必须决定它们的访问和操作语法和语义。在这种情况下,记录类型本质上可以被视为一个异构列表,就像struct模块的unpack方法返回的元组一样;一个1D记录数组可以被解释为2D数组,其中第二个维度是字段列表的索引。这种增强的数组语义使得访问一个或多个字段的数组变得容易和直接。它还允许用户以自然直观的方式对字段执行数组操作。如果我们假设记录实现为数组类型,则最后一个维度默认为0,因此对于由简单类型(如数值)组成的数组可以忽略。
- 记录语义如何定义和实现?
- 掩码数组如何实现?
Numeric 1 中的掩码数组作为单独的数组类实现。随着 Numeric 2 中添加新数组类型的能力,Numeric 2 中的掩码数组可能作为新的数组类型而不是数组类来实现。
- 数值误差(特别是 IEEE 浮点误差)如何处理?
提议者(Paul Barrett 和 Travis Oliphant)尚不清楚处理误差的最佳或首选方式。由于大多数执行操作的C函数迭代数组的最内层(最后一个)维度。这个维度可能包含一千个或更多项目,这些项目可能具有一种或多种不同类型的误差,例如除以零、下溢和上溢。此外,跟踪这些误差可能会以牺牲性能为代价。因此,我们提出以下几种选择
- 打印最严重错误的 сообщения,由用户自行定位错误。
- 打印所有发生的错误消息及其发生次数,由用户自行定位错误。
- 打印所有发生的错误消息及其发生位置列表。
- 或者采用混合方法,只打印最严重的错误,但同时跟踪错误的类型和位置。这将允许用户定位错误,同时保持错误消息简洁。
- 需要哪些功能来简化FORTRAN库和代码的集成?
在此阶段,考虑如何简化FORTRAN库和用户代码在Numeric 2中的集成是一个好主意。
实施步骤
- 实现基本的UFunc功能
- 最小数组类
必要的类属性和方法,例如.shape、.data、.type等。
- 最小ArrayType类
Int32、Real64、Complex64、Char、Object
- 最小UFunc类
UFunc实例化、C函数注册、1D数组的UFunc调用,包括对齐、字节交换和强制类型转换的规则。
- 最小C扩展模块
_UFunc,它在C中执行最内层数组循环。
此步骤实现了执行:“c = add(a, b)”所需的一切,其中a、b和c是1D数组。它教会我们如何添加新的UFuncs、强制转换数组、将必要信息传递给C迭代器方法以及实际执行计算。
- 最小数组类
- 继续增强UFunc迭代器和Array类
- 实现Array类的一些访问方法:print、repr、getitem、setitem等。
- 实现多维数组
- 使用UFunc实现一些基本的Array方法:+、-、*、/等。
- 使UFuncs能够使用Python序列。
- 完成标准UFunc和Array类行为
- 实现getslice和setslice行为
- 研究数组广播规则
- 实现Record类型
- 添加附加功能
- 添加更多UFuncs
- 实现缓冲区或mmap访问
不兼容性
以下是Numeric 1和Numeric 2之间行为不兼容之处的列表。
- 标量强制转换规则
Numeric 1对数组和Python数值类型有一套单一的强制转换规则。这可能在数组表达式的计算过程中导致意想不到且令人烦恼的问题。Numeric 2旨在通过拥有两套强制转换规则来克服这些问题:一套用于数组和Python数值类型,另一套仅用于数组。
- 无savespace属性
Numeric 1中的savespace属性使得设置了此属性的数组优先于未设置此属性的数组。Numeric 2将不会有此类属性,因此正常的数组强制转换规则将生效。
- 切片语法返回副本
Numeric 1 中的切片语法返回原始数组的视图。Numeric 2 的切片行为将是副本。您应该使用 ArrayView 类来获取数组的视图。
- 布尔比较返回布尔数组
由于Python当前的限制,Numeric 1中数组之间的比较结果是布尔标量。Python 2.1中Rich Comparisons的出现将允许返回布尔数组。
- 类型字符已弃用
Numeric 2 将拥有一个由类型实例组成的 ArrayType 类,例如 Int8、Int16、Int32 和用于带符号整数的 Int。Numeric 1 中的类型代码方案将可用于向后兼容,但将被弃用。
附录
- 隐式子数组迭代
计算机动画由多个形状相同的二维图像或帧组成。通过将这些图像堆叠成一个内存块,创建了一个三维数组。然而,要执行的操作并非针对整个三维数组,而是针对二维子数组集。在大多数数组语言中,每个帧都必须提取、操作,然后使用类似for循环的方式重新插入到输出数组中。J语言允许程序员通过为帧和数组设置秩来隐式执行此类操作。默认情况下,这些秩在创建数组时将相同。Numeric 1开发人员曾打算实现此功能,因为它基于J语言。Numeric 1代码具有实现此行为所需的变量,但从未实现。如果Numeric 1中的数组广播规则不能完全支持此行为,我们打算在Numeric 2中实现隐式子数组迭代。
版权
本文档已置于公共领域。
参考资料
来源:https://github.com/python/peps/blob/main/peps/pep-0209.rst