PEP 296 – 添加字节对象类型
- 作者:
- Scott Gilbert <xscottg at yahoo.com>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建日期:
- 2002年7月12日
- Python 版本:
- 2.3
- 发布历史:
注意
本PEP已由作者撤回(以支持PEP 358)。
摘要
本PEP提议创建一个新的标准类型和内置构造函数,名为“bytes”。bytes对象是一个高效存储的字节数组,具有一些附加特性,使其与几种类似的实现区分开来。
基本原理
Python目前有许多对象实现了类似于本提案中bytes对象的特性。例如,标准字符串、缓冲区、数组和mmap对象在某些方面都与bytes对象非常相似。此外,几个重要的第三方扩展也创建了类似的对象,以尝试满足类似的需求。令人沮丧的是,这些对象中的每一个都范围过窄,并且缺少关键特性,使其无法应用于更广泛的问题类别。
规范
bytes对象具有以下重要特性:
- 通过标准C类型“unsigned char”实现高效的底层数组存储。这允许对分配的内存量进行精细控制。根据下一项中指定的对齐限制,低级扩展可以根据需要轻松地将指针转换为不同的类型。
此外,由于该对象是作为字节数组实现的,因此可以将bytes对象传递给标准库中已有的、目前与字符串一起使用的广泛例程库。例如,bytes对象与struct模块结合使用,可以仅使用Python脚本为array模块提供完整的替代方案。
如果出现一个不寻常的平台,一个没有原生无符号8位类型的平台,该对象将尽力在Python脚本级别上表示自己,就好像它是一个8位无符号值数组一样。许多扩展能否正确处理这一点值得怀疑,但在这些情况下,Python脚本可能是可移植的。
- 分配的字节数组的对齐方式由平台实现malloc所承诺的决定。可以提供由扩展创建的bytes对象,该对象提供扩展作者认为合适的任意对齐方式。
此对齐限制应允许bytes对象用作所有标准C类型的存储空间——包括
PyComplex对象或其他标准C类型结构体。如有必要,扩展可以提供进一步的对齐限制。 - bytes对象实现字符串/数组对象提供的一部分序列操作,但在某些情况下语义略有不同。特别是,切片总是返回一个新的bytes对象,但底层内存在这两个对象之间共享。这种切片行为被称为创建“视图”。此外,bytes对象的重复和连接是未定义的,并将引发异常。
由于这些对象很可能用于高性能应用程序,因此决定使用视图切片的一个动机是,bytes对象之间的复制应该非常高效,并且不需要创建临时对象。以下代码说明了这一点:
# create two 10 Meg bytes objects b1 = bytes(10000000) b2 = bytes(10000000) # copy from part of one to another with out creating a 1 Meg temporary b1[2000000:3000000] = b2[4000000:5000000]
如果切片赋值的右值与左值的长度不同,将引发异常。但是,切片赋值将正确处理重叠的切片(通常使用memmove实现)。
- bytes对象将被
pickle和cPickle模块识别为原生类型,以实现高效序列化。(事实上,这是唯一无法通过第三方扩展实现的要求。)过去已经实现了一些部分解决方案,以解决序列化存储在类字节对象中的数据而无需创建数据临时副本到字符串中的需求。array对象的tofile和fromfile方法就是很好的例子。bytes对象也将支持这些方法。然而,pickling在其他情况下也很有用——例如在shelve模块中,或实现Python对象的RPC,而要求最终用户使用两种不同的序列化机制来高效传输数据是不可取的。
XXX:将尝试以一种方式实现新bytes对象的pickling,以便以前版本的Python将其解pickle为字符串对象。
在解pickle时,bytes对象将从Python分配的内存中创建(通过
malloc)。因此,它将失去扩展提供的指针可能提供的任何附加属性(特殊对齐或特殊类型的内存)。XXX:将尝试使bytes类型的C子类能够提供将被解pickle到的内存。例如,一个名为PageAlignedBytes的派生类将解pickle到同样是页对齐的内存中。
在任何int为32位的平台上(大多数平台),目前无法创建长度大于31位可表示的字符串。因此,当操作不可能时,pickling到字符串将引发异常。
至少在支持大文件的平台上(许多平台),通过重复调用
file.write()方法,将大型bytes对象pickle到文件应该成为可能。 - bytes类型支持
PyBufferProcs接口,但bytes对象提供了额外的保证,即只要持有对bytes对象的引用,指针就不会被解除分配或重新分配。这意味着bytes对象一旦创建就不可调整大小,但如果PyBytes_Check(...)测试通过,则可以在单独的线程操作指针指向的内存时释放全局解释器锁(GIL)。bytes对象的这一特性使其可用于异步文件I/O或多处理器机器等情况,其中通过
PyBufferProcs获得的指针将独立于全局解释器锁使用。知道在GIL释放后指针不能被重新分配或释放,使扩展作者能够获得真正的并发性,并利用额外的处理器进行指针上的长时间计算。
- 在C/C++扩展中,bytes对象可以从提供的指针和析构函数创建,以便在引用计数降至零时释放内存。
bytes对象切片的特殊实现允许多个bytes对象引用相同的指针/析构函数。因此,将对实际的指针/析构函数保持一个引用计数。此引用计数独立于通常与Python对象关联的引用计数。
XXX:可能希望将内部的引用计数对象作为实际的Python对象暴露出来。如果出现一个好的用例,应该可以在以后实现这一点,而不会损失向后兼容性。
- 还可以将bytes对象标记为只读,在这种情况下它实际上是不可变的,但提供了bytes对象的其他特性。
- bytes对象使用Python
LONG_LONG类型跟踪其数据的长度。尽管PyBufferProcs的当前定义将长度限制为int的大小,但本PEP不提议对此进行任何更改。相反,扩展可以通过显式调用PyBytes_Check(...)来绕过此限制,如果成功,它们可以调用PyBytes_GetReadBuffer(...)或PyBytes_GetWriteBuffer来获取对象的指针和完整长度作为LONG_LONG。如果使用标准的
PyBufferProcs机制,并且bytes对象的大小大于int所能表示的大小,则bytes对象将引发异常。从Python脚本来看,bytes对象将可以使用long进行下标,从而避免32位int限制。
仍存在
len()函数的问题,因为它返回的是PyObject_Size(),而后者也返回int。作为权宜之计,bytes对象将提供一个.length()方法,该方法将返回一个long。 - bytes对象可以在Python脚本级别通过向bytes构造函数传递一个int/long来构造,表示要分配的字节数。例如:
b = bytes(100000) # alloc 100K bytes
构造函数还可以接受另一个bytes对象。这对于实现unpickling,以及将读写bytes对象转换为只读bytes对象将很有用。可选的第二个参数将用于指定创建只读bytes对象。
- 从C API,bytes对象可以使用以下任何签名进行分配:
PyObject* PyBytes_FromLength(LONG_LONG len, int readonly); PyObject* PyBytes_FromPointer(void* ptr, LONG_LONG len, int readonly void (*dest)(void *ptr, void *user), void* user);
在
PyBytes_FromPointer(...)函数中,如果将dest函数指针作为NULL传入,则不会调用它。这应该只用于从静态分配的空间创建bytes对象。用户指针在其他地方被称为闭包。它是一个用户可以用于任何目的的指针。它将在清理时传递给析构函数,并且可以用于许多事情。如果不需要用户指针,则应传递
NULL。 - bytes类型将是一个新式类,因为所有标准Python类型似乎都朝着这个方向发展。
与现有类型的对比
解决缺少bytes对象的最常见方法是简单地使用字符串对象代替。二进制文件、struct/array模块以及其他几个例子都存在这种情况。撇开这些用法通常与文本字符串无关的风格问题不谈,存在一个实际问题:字符串是不可变的,因此无法直接操作这些情况下返回的数据。此外,字符串模块中的许多优化(例如缓存哈希值或内联指针)意味着如果扩展作者试图打破字符串对象的规则,他们将面临非常大的风险。
缓冲区对象似乎旨在解决bytes对象试图实现的目的,但其实现中的一些缺点[1]使其在许多常见情况下不太有用。缓冲区对象在切片行为上做出了不同的选择(它返回新字符串而不是缓冲区用于切片和其他操作),并且它没有像bytes对象那样做出许多关于对齐或能够释放GIL的承诺。
此外,关于缓冲区对象,不可能简单地用bytes对象替换缓冲区对象并保持向后兼容性。缓冲区对象提供了一种机制,可以将另一个对象的PyBufferProcs提供的指针作为自己的指针呈现。由于无法保证另一个对象的行为遵循与bytes对象相同的严格规则集,因此它不能在bytes对象可以使用的位置使用。
array模块支持创建字节数组,但它不提供C API来向扩展提供的内存提供指针和析构函数。这使得它无法用于从共享内存或具有特殊对齐或锁定以进行DMA传输的内存中构造对象。此外,array对象目前无法pickle。最后,由于array对象允许其内容通过extend方法增长,因此如果在使用时未持有GIL,指针可能会更改。
从array对象创建缓冲区对象存在相同的问题,即当array对象调整大小时会留下无效指针。
mmap对象满足其特定利基,但并未尝试解决更广泛的问题。
最后,任何第三方扩展都无法在不创建标准Python类型的临时对象的情况下实现pickling。例如,在Numeric社区中,大型数组无法在不创建大型二进制字符串来复制数组数据的情况下进行pickle,这是令人不快的。
向后兼容性
作者意识到的唯一可能导致向后兼容性问题的情况是,早期版本的Python尝试解pickle包含新bytes类型的数据。
参考实现
XXX:实际实施正在进行中,但随着本PEP的进一步审查,仍有可能进行更改。
以下新文件将添加到Python基线中:
Include/bytesobject.h # C interface
Objects/bytesobject.c # C implementation
Lib/test/test_bytes.py # unit testing
Doc/lib/libbytes.tex # documentation
以下文件也将被修改:
Include/Python.h # adding bytesmodule.h include file
Python/bltinmodule.c # adding the bytes type object
Modules/cPickle.c # adding bytes to the standard types
Lib/pickle.py # adding bytes to the standard types
有可能可以清理并使用bytes对象实现其他几个模块。mmap模块首先想到,但如上所述,可以将其重新实现为纯Python模块。虽然本PEP能够减少一些源代码量很有吸引力,但作者认为这可能会给现有应用程序带来不必要的破坏风险,目前应避免这样做。
其他注意事项/评论
- Guido van Rossum想知道是否可以从mmap对象创建bytes对象。mmap对象似乎支持为bytes对象提供内存所需的条件。(它不调整大小,并且指针在对象的生命周期内有效。)因此,可以在mmap模块中添加一个方法,以便可以直接从mmap对象创建bytes对象。这种实现方式的初步尝试是使用上面描述的
PyBytes_FromPointer()函数,并将mmap_object作为用户指针传递。析构函数将对mmap_object进行递减引用计数以进行清理。 - Todd Miller指出,拥有两个新函数可能很有用:
PyObject_AsLargeReadBuffer()和PyObject_AsLargeWriteBuffer,它们类似于PyObject_AsReadBuffer()和PyObject_AsWriteBuffer(),但除了void*指针之外,还支持获取LONG_LONG长度。这些函数将允许扩展作者透明地使用bytes对象(支持LONG_LONG长度)和大多数其他类似缓冲区的对象(仅支持int长度)。这些函数可以代替或补充创建特定的PyByte_GetReadBuffer()和PyBytes_GetWriteBuffer()函数。XXX:作者认为这是一个非常好的主意,因为它为其他对象最终支持大(64位)指针铺平了道路,并且它应该只影响abstract.c和abstract.h。这应该添加到上面吗?
- 人们普遍认为,滥用
PyBufferProcs接口的段计数不是一个解决31位长度限制的好办法。如果你不知道这意味着什么,那么你有很多同伴。Python基线中的大多数代码,以及可能许多第三方扩展中的代码,在段计数不是1时都会放弃。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0296.rst