Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 296 – 添加 bytes 对象类型

作者:
Scott Gilbert <xscottg at yahoo.com>
状态:
已撤回
类型:
标准跟踪
创建:
2002年7月12日
Python 版本:
2.3
历史记录:


目录

通知

此 PEP 由作者撤回(转而支持 PEP 358)。

摘要

此 PEP 提出创建一个新的标准类型和内置构造函数,称为“bytes”。bytes 对象是一个高效存储的字节数组,具有一些其他特性,使其有别于一些类似的实现。

基本原理

Python 目前有很多对象实现了类似于此提案中 bytes 对象的功能。例如,标准字符串、缓冲区、数组和 mmap 对象在某些方面都与 bytes 对象非常相似。此外,一些重要的第三方扩展创建了类似的对象来尝试满足类似的需求。令人沮丧的是,这些对象中的每一个范围都太窄,并且缺少关键特性,使其无法应用于更广泛的问题类别。

规范

bytes 对象具有以下重要特性

  1. 通过标准 C 类型“unsigned char”进行高效的基础数组存储。这允许对分配多少内存进行细粒度控制。通过下一项中指定的对齐限制,低级扩展可以轻松地根据需要将指针转换为不同的类型。

    此外,由于对象实现为字节数组,因此可以将 bytes 对象传递给标准库中已有的广泛例程,这些例程目前使用字符串。例如,bytes 对象结合 struct 模块可用于仅使用 Python 脚本提供 array 模块的完整替换。

    如果出现一个不寻常的平台,其中没有本机无符号 8 位类型,则该对象将尽最大努力在 Python 脚本级别将自身表示为无符号 8 位值的数组。许多扩展是否可以正确处理这一点尚不清楚,但在这些情况下,Python 脚本可能是可移植的。

  2. 分配的字节数组的对齐方式是 malloc 的平台实现承诺的任何内容。可以从提供任何任意对齐方式的扩展创建 bytes 对象,扩展作者可以根据需要进行提供。

    此对齐限制应允许 bytes 对象用作所有标准 C 类型的存储 - 包括 PyComplex 对象或其他标准 C 类型类型的结构。扩展可以根据需要提供进一步的对齐限制。

  3. 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 实现)。

  4. bytes 对象将被 picklecPickle 模块识别为原生类型,以进行高效的序列化。(实际上,这是唯一无法通过第三方扩展实现的要求。)

    过去已实现了一些部分解决方案来解决在不创建数据到字符串的临时副本的情况下序列化存储在类似字节的对象中的数据的需求。array 对象的 tofile 和 fromfile 方法就是很好的例子。bytes 对象也将支持这些方法。但是,pickle 在其他情况下也很有用 - 例如在 shelve 模块中或实现 Python 对象的 RPC,并且要求最终用户使用两种不同的序列化机制来获得高效的数据传输是不希望的。

    XXX:将尝试以使 Python 的早期版本将其解组为字符串对象的方式实现新 bytes 对象的 pickle。

    在解组时,将从 Python 分配的内存(通过 malloc)创建 bytes 对象。因此,它将丢失扩展提供的指针可能具有的任何其他属性(特殊对齐或特殊类型的内存)。

    XXX:将尝试使其成为 bytes 类型的 C 子类可以提供将被解组到的内存。例如,名为 PageAlignedBytes 的派生类将解组到也是页面对齐的内存。

    在 int 为 32 位(大多数)的任何平台上,当前无法创建长度大于 31 位所能表示的字符串。因此,当操作不可行时,pickle 到字符串将引发异常。

    至少在支持大文件的平台(其中许多)上,应该可以通过对 file.write() 方法的重复调用将大型 bytes 对象 pickle 到文件中。

  5. bytes 类型支持 PyBufferProcs 接口,但 bytes 对象提供了额外的保证,即只要持有对 bytes 对象的引用,指针就不会被释放或重新分配。这意味着一旦创建,bytes 对象就不能调整大小,但允许在单独的线程操作指向的内存时释放全局解释器锁 (GIL),如果 PyBytes_Check(...) 测试通过。

    bytes 对象的此特性允许它用于异步文件 I/O 或在多处理器机器上的情况,其中通过 PyBufferProcs 获得的指针将独立于全局解释器锁使用。

    知道在释放 GIL 后不能重新分配或释放指针使扩展作者能够获得真正的并发性,并利用其他处理器进行对指针的长时间运行计算。

  6. 在 C/C++ 扩展中,可以从提供的指针和析构函数创建 bytes 对象,以在引用计数变为零时释放内存。

    bytes 对象切片的特殊实现允许多个 bytes 对象引用同一个指针/析构函数。因此,将在实际指针/析构函数上保留一个引用计数。此引用计数与通常与 Python 对象关联的引用计数分开。

    XXX:可能希望将内部引用计数对象公开为实际的 Python 对象。如果出现好的用例,则以后可以实现此功能,而不会造成向后兼容性的损失。

  7. 还可以将 bytes 对象指定为只读,在这种情况下,它实际上不可变,但确实提供了 bytes 对象的其他功能。
  8. bytes 对象使用 Python LONG_LONG 类型跟踪其数据的长度。即使 PyBufferProcs 的当前定义将长度限制为 int 的大小,此 PEP 也不建议在那里进行任何更改。相反,扩展可以通过进行显式的 PyBytes_Check(...) 调用来解决此限制,如果成功,则可以进行 PyBytes_GetReadBuffer(...)PyBytes_GetWriteBuffer 调用以获取指针和对象的完整长度作为 LONG_LONG

    如果使用标准 PyBufferProcs 机制并且 bytes 对象的大小大于整数所能表示的大小,则 bytes 对象将引发异常。

    从 Python 脚本中,可以使用长整数对 bytes 对象进行下标访问,因此可以避免 32 位 int 限制。

    len() 函数仍然存在问题,因为它为 PyObject_Size(),并且它也返回 int。作为解决方法,bytes 对象将提供一个 .length() 方法,该方法将返回一个长整数。

  9. 可以通过将 int/长整数传递给 bytes 构造函数以及要分配的字节数来在 Python 脚本级别构造 bytes 对象。例如
    b = bytes(100000) # alloc 100K bytes
    

    构造函数还可以接受另一个 bytes 对象。这将对 pickle 的实现以及将读写 bytes 对象转换为只读对象很有用。可选的第二个参数将用于指定只读 bytes 对象的创建。

  10. 从 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

  11. bytes 类型将是一个新式类,因为这似乎是所有标准 Python 类型的走向。

与现有类型的对比

解决缺少 bytes 对象的最常见方法是简单地使用字符串对象代替它。二进制文件、struct/array 模块以及其他几个示例都存在这种情况。撇开这些用法通常与文本字符串无关的样式问题不谈,还存在字符串不可变的实际问题,因此无法直接操作这些情况下返回的数据。此外,字符串模块中的许多优化(例如缓存哈希值或内部化指针)意味着如果扩展作者试图违反字符串对象的规则,他们将处于非常危险的境地。

缓冲区对象看起来像是为了解决 bytes 对象试图实现的目的而设计的,但其实现中的几个缺点 [1] 使其在许多常见情况下变得不太有用。缓冲区对象对其切片行为做出了不同的选择(它返回新的字符串而不是用于切片和其他操作的缓冲区),并且它没有对对齐或能够释放 GIL 做出与 bytes 对象相同的许多承诺。

关于缓冲区对象,也不可能简单地用字节对象替换缓冲区对象并保持向后兼容性。缓冲区对象提供了一种机制,可以获取另一个对象的PyBufferProcs提供的指针,并将其呈现为自己的指针。由于不能保证其他对象的行為遵循与字节对象相同的严格规则集,因此它不能用于字节对象可以使用的场合。

数组模块支持创建字节数组,但它没有提供用于向扩展提供的内存提供指针和析构函数的 C API。这使得它无法用于构建来自共享内存的对象,或者对像 DMA 传输这样的东西具有特殊对齐或锁定的内存。此外,数组对象目前不进行序列化。最后,由于数组对象允许其内容通过 extend 方法增长,因此如果在使用它时未持有 GIL,则指针可能会发生变化。

从数组对象创建缓冲区对象存在同样的问题,即在调整数组对象大小时会留下无效指针。

mmap 对象满足其特定的需求,但并不试图解决更广泛的问题类别。

最后,任何第三方扩展都不能在不创建标准 Python 类型的临时对象的情况下实现序列化。例如,在 Numeric 社区中,一个大型数组在不创建大型二进制字符串来复制数组数据的情况下无法序列化,这一点令人不快。

向后兼容性

作者知道的唯一可能导致向后兼容性问题的可能性是在以前版本的 Python 中尝试反序列化包含新字节类型的数据。

参考实现

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

可能还有其他几个模块可以清理并根据字节对象实现。首先想到的是 mmap 模块,但如上所述,可以将数组模块重新实现为纯 Python 模块。虽然这个 PEP 实际上可以减少一些源代码量这一点很有吸引力,但作者认为这可能会给破坏现有应用程序带来不必要的风险,因此目前应避免这样做。

其他说明/评论

  • Guido van Rossum 思考是否可以从 mmap 对象创建字节对象。mmap 对象似乎支持为字节对象提供内存所需的条件。(它不调整大小,并且指针在对象的生命周期内有效。)因此,可以向 mmap 模块添加一个方法,以便可以直接从 mmap 对象创建字节对象。最初尝试如何实现这一点的方法是使用上面描述的PyBytes_FromPointer()函数,并将mmap_object作为用户指针传递。析构函数将对mmap_object进行 decref 操作以进行清理。
  • Todd Miller 指出,拥有两个新函数可能很有用:PyObject_AsLargeReadBuffer()PyObject_AsLargeWriteBuffer,它们类似于 PyObject_AsReadBuffer()PyObject_AsWriteBuffer(),但除了void*指针之外,还支持获取LONG_LONG长度。这些函数将允许扩展作者透明地使用字节对象(支持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

上次修改时间: 2023-09-09 17:39:29 GMT