PEP 688 – 让缓冲区协议在 Python 中可访问
- 作者:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2022年4月23日
- Python 版本:
- 3.12
- 发布历史:
- 2022年4月23日, 2022年4月25日, 2022年10月6日, 2022年10月26日
- 决议:
- 2023年3月7日
摘要
本 PEP 提出了缓冲区协议的 Python 级别 API,该协议目前只能通过 C 代码访问。这允许类型检查器评估对象是否实现了该协议。
动机
CPython C API 提供了一种灵活的机制来访问对象的底层内存——缓冲区协议,该协议在 PEP 3118 中引入。接受二进制数据的函数通常被编写为处理任何实现缓冲区协议的对象。例如,在撰写本文时,CPython 中大约有 130 个函数使用 Argument Clinic 的 Py_buffer 类型,该类型接受缓冲区协议。
目前,Python 代码无法检查对象是否支持缓冲区协议。此外,静态类型系统不提供类型注解来表示该协议。这在为接受通用缓冲区的代码编写类型注解时是一个常见问题。
同样,用 Python 编写的类无法支持缓冲区协议。Python 中的缓冲区类将使用户能够轻松地包装 C 缓冲区对象,或测试使用缓冲区协议的 API 的行为。诚然,这不是一个特别常见的需求。然而,自 2012 年以来,一直存在一项关于支持用 Python 编写的缓冲区类的 CPython 功能请求,该请求至今仍未解决。
基本原理
当前选项
有两种已知的变通方法用于在类型系统中注解缓冲区类型,但两者都不足。
首先,typeshed 中缓冲区类型的当前变通方法 是一个类型别名,它列出了标准库中众所周知的缓冲区类型,例如 bytes、bytearray、memoryview 和 array.array。这种方法适用于标准库,但它不适用于第三方缓冲区类型。
其次,typing.ByteString 的文档目前声明
此类型表示字节序列的bytes、bytearray和memoryview类型。作为此类型的简写,
bytes可用于注解上述任何类型的参数。
尽管这句话自 2015 年 以来就出现在文档中,但在任何类型 PEP 中都没有指定使用 bytes 来包含这些其他类型。此外,这种机制存在许多问题。它不包括所有可能的缓冲区类型,并且它使 bytes 类型在类型注解中变得模糊。毕竟,许多操作对于 bytes 对象是有效的,但对于 memoryview 对象则无效,并且函数完全可能接受 bytes 但不接受 memoryview 对象。mypy 用户报告说,这种快捷方式给 psycopg 项目造成了严重问题。
缓冲区类型
C 缓冲区协议支持许多选项,影响步长、连续性和对缓冲区写入的支持。其中一些选项在类型系统中会很有用。例如,typeshed 目前为可写和只读缓冲区提供了单独的类型别名。
然而,在 C 缓冲区协议中,大多数这些选项不能直接在类型对象上查询。要弄清对象是否支持特定标志的唯一方法是实际请求缓冲区。对于某些类型,例如 memoryview,支持的标志取决于实例。因此,在类型系统中表示对这些标志的支持将很困难。
规范
Python 级别的缓冲区协议
我们建议添加两个 Python 级别的特殊方法,__buffer__ 和 __release_buffer__。实现这些方法的 Python 类可以作为 C 代码的缓冲区使用。反之,用 C 实现的支持缓冲区协议的类将获得可从 Python 代码访问的合成方法。
__buffer__ 方法用于从 Python 对象创建缓冲区,例如通过 memoryview() 构造函数。它对应于 bf_getbuffer C 槽。此方法的 Python 签名是 def __buffer__(self, flags: int, /) -> memoryview: ...。该方法必须返回一个 memoryview 对象。如果在具有 __buffer__ 方法的 Python 类上调用 bf_getbuffer 槽,解释器将从该方法返回的 memoryview 中提取底层的 Py_buffer 并将其返回给 C 调用者。同样,如果 Python 代码在实现 bf_getbuffer 的 C 类实例上调用 __buffer__ 方法,则返回的缓冲区将被包装在 memoryview 中,以供 Python 代码使用。
当调用者不再需要 __buffer__ 返回的缓冲区时,应调用 __release_buffer__ 方法。它对应于 bf_releasebuffer C 槽。这是缓冲区协议的可选部分。此方法的 Python 签名是 def __release_buffer__(self, buffer: memoryview, /) -> None: ...。要释放的缓冲区包装在 memoryview 中。当通过 CPython 的缓冲区 API 调用此方法时(例如,通过在由 __buffer__ 返回的 memoryview 上调用 memoryview.release),传递的 memoryview 与 __buffer__ 返回的对象相同。也可以在实现 bf_releasebuffer 的 C 类上调用 __release_buffer__。
如果对象上存在 __release_buffer__,则直接在该对象上调用 __buffer__ 的 Python 代码必须在处理完缓冲区后在该同一对象上调用 __release_buffer__。否则,对象使用的资源可能无法回收。同样,在没有先前调用 __buffer__ 的情况下调用 __release_buffer__,或者对单个 __buffer__ 调用多次调用它,都是编程错误。对于实现 C 缓冲区协议的对象,如果参数不是包装同一对象的 memoryview,则对 __release_buffer__ 的调用将引发异常。在有效调用 __release_buffer__ 后,memoryview 将失效(如同其 release() 方法已被调用),并且后续对同一 memoryview 的 __release_buffer__ 调用将引发异常。解释器将确保 Python API 的误用不会破坏 C 级别的不变性——例如,它不会导致内存安全违规。
inspect.BufferFlags
为了帮助实现 __buffer__,我们添加了 inspect.BufferFlags,它是 enum.IntFlag 的子类。此枚举包含 C 缓冲区协议中定义的所有标志。例如,inspect.BufferFlags.SIMPLE 的值与 PyBUF_SIMPLE 常量相同。
collections.abc.Buffer
我们添加了一个新的抽象基类 collections.abc.Buffer,它要求实现 __buffer__ 方法。该类主要用于类型注解。
def need_buffer(b: Buffer) -> memoryview:
return memoryview(b)
need_buffer(b"xy") # ok
need_buffer("xy") # rejected by static type checkers
它也可以用于 isinstance 和 issubclass 检查
>>> from collections.abc import Buffer
>>> isinstance(b"xy", Buffer)
True
>>> issubclass(bytes, Buffer)
True
>>> issubclass(memoryview, Buffer)
True
>>> isinstance("xy", Buffer)
False
>>> issubclass(str, Buffer)
False
在 typeshed 存根文件中,该类应定义为 Protocol,遵循 collections.abc 中其他简单 ABC(例如 collections.abc.Iterable 或 collections.abc.Sized)的先例。
示例
以下是一个实现缓冲区协议的 Python 类示例
import contextlib
import inspect
class MyBuffer:
def __init__(self, data: bytes):
self.data = bytearray(data)
self.view = None
def __buffer__(self, flags: int) -> memoryview:
if flags != inspect.BufferFlags.FULL_RO:
raise TypeError("Only BufferFlags.FULL_RO supported")
if self.view is not None:
raise RuntimeError("Buffer already held")
self.view = memoryview(self.data)
return self.view
def __release_buffer__(self, view: memoryview) -> None:
assert self.view is view # guaranteed to be true
self.view.release()
self.view = None
def extend(self, b: bytes) -> None:
if self.view is not None:
raise RuntimeError("Cannot extend held buffer")
self.data.extend(b)
buffer = MyBuffer(b"capybara")
with memoryview(buffer) as view:
view[0] = ord("C")
with contextlib.suppress(RuntimeError):
buffer.extend(b"!") # raises RuntimeError
buffer.extend(b"!") # ok, buffer is no longer held
with memoryview(buffer) as view:
assert view.tobytes() == b"Capybara!"
旧版 Python 的等效方案
新的类型特性通常会被反向移植到旧版 Python 中,通过 typing_extensions 包实现。由于缓冲区协议目前只能在 C 中访问,本 PEP 无法在像 typing_extensions 这样的纯 Python 包中完全实现。作为临时解决方案,将为没有 collections.abc.Buffer 的 Python 版本提供一个抽象基类 typing_extensions.Buffer。
在本 PEP 实现后,为了表明对象支持缓冲区协议,不再需要从 collections.abc.Buffer 继承。然而,在旧版 Python 中,为了向类型检查器表明一个类支持缓冲区协议,将需要显式地从 typing_extensions.Buffer 继承,因为支持缓冲区协议的对象将没有 __buffer__ 方法。预计这主要发生在存根文件中,因为缓冲区类必然用 C 代码实现,而 C 代码不能内联定义类型。对于运行时使用,可以使用 ABC.register API 将缓冲区类注册到 typing_extensions.Buffer。
bytes 无特殊含义
在 typing 文档中,关于 bytes 可用作其他 ByteString 类型的简写的特殊情况将被删除。有了 collections.abc.Buffer 作为替代方案,将不再有充分的理由允许 bytes 作为简写。目前实现此行为的类型检查器应弃用并最终移除它。
向后兼容性
__buffer__ 和 __release_buffer__ 属性
由于本 PEP 中的运行时更改仅添加了新功能,因此几乎没有向后兼容性问题。
但是,将 __buffer__ 或 __release_buffer__ 属性用于其他目的的代码可能会受到影响。虽然所有双下划线方法从技术上讲都为语言保留,但仍建议确保新的双下划线方法不会与过多现有代码(尤其是广泛使用的包)冲突。一项对公开可访问代码的调查发现:
- PyPy 支持
__buffer__方法,其语义与本 PEP 中提出的兼容。一位 PyPy 核心开发人员表达了对本 PEP 的支持。 - pyzmq 实现了一个与 PyPy 兼容的
__buffer__方法。 - mpi4py 定义了一个
SupportsBuffer协议,它将等同于本 PEP 的collections.abc.Buffer。 - NumPy 曾经有一个未记录的行为,它会访问
__buffer__属性(而不是方法)来获取对象的缓冲区。此行为在 2019 年为 NumPy 1.17 移除。此行为最后一次工作是在 NumPy 1.16 中,该版本仅支持 Python 3.7 及更早版本。到本 PEP 预计实现时,Python 3.7 将已达到其生命周期结束。
因此,本 PEP 使用 __buffer__ 方法将提高与 PyPy 的互操作性,并且不会干扰任何主要 Python 包的当前版本。
没有公开可访问的代码使用名称 __release_buffer__。
移除 bytes 的特殊情况
此外,在类型检查器中删除 bytes 特殊行为的建议确实对其用户产生了向后兼容性影响。一项对 mypy 的实验表明,如果删除 bytes 提升,几个使用 mypy 进行类型检查的主要开源项目将出现新错误。其中许多错误可以通过改进 typeshed 中的存根来修复,例如 builtins、binascii、pickle 和 re 模块已完成此操作。对 typeshed 中 bytes 类型所有用法的审查正在进行中。总的来说,这一更改提高了类型安全性和类型系统的一致性,因此我们认为迁移成本是值得的。
如何教授此内容
我们将在文档的适当位置添加指向 collections.abc.Buffer 的注释,例如 typing.python.org 和 mypy 备忘单。类型检查器可以在其错误消息中提供额外的提示。例如,当它们遇到将缓冲区对象传递给仅接受 bytes 的函数时,错误消息可以包含一个注释,建议改用 collections.abc.Buffer。
参考实现
本 PEP 的实现已在作者的分支中提供。
被拒绝的想法
types.Buffer
本 PEP 的早期版本曾提议添加一个新的 types.Buffer 类型,其 __instancecheck__ 用 C 实现,以便 isinstance() 检查可用于检查类型是否实现了缓冲区协议。这避免了将完整的缓冲区协议暴露给 Python 代码的复杂性,同时仍然允许类型系统检查缓冲区协议。
然而,那种方法与其他类型系统不兼容,因为 types.Buffer 将是一个名义类型,而不是结构类型。例如,将无法表示“同时支持缓冲区协议和 __len__ 的对象”。根据当前的提议,__buffer__ 就像任何其他特殊方法一样,因此可以定义一个 Protocol 将其与其他方法结合起来。
更一般地,Python 的其他部分都没有像提议的 types.Buffer 那样工作。当前的提案与语言的其余部分更为一致,其中 C 级别的槽通常具有相应的 Python 级别的特殊方法。
保持 bytearray 与 bytes 兼容
有人建议删除 memoryview 总是与 bytes 兼容的特殊情况,但保留 bytearray 的兼容性,因为这两种类型具有非常相似的接口。然而,一些标准库函数(例如,re.compile、socket.getaddrinfo,以及大多数接受路径类参数的函数)接受 bytes 而不接受 bytearray。在大多数代码库中,bytearray 也不是一个非常常见的类型。我们更倾向于让用户明确地拼写出接受的类型(或者如果只需要一组特定的方法,则使用 PEP 544 中的 Protocol)。提案的这方面内容在 typing-sig 邮件列表中专门讨论过,类型社区没有提出任何强烈异议。
区分可变和不可变缓冲区
缓冲区类型中最常用的区别是缓冲区是否可变。某些函数只接受可变缓冲区(例如,bytearray,某些 memoryview 对象),而其他函数则接受所有缓冲区。
本 PEP 的早期版本曾提议使用 bf_releasebuffer 槽的存在来确定缓冲区类型是否可变。此规则适用于大多数标准库缓冲区类型,但可变性与此槽的存在之间的关系并非绝对。例如,numpy 数组是可变的,但没有此槽。
当前的缓冲区协议无法可靠地确定缓冲区类型是可变还是不可变缓冲区。因此,本 PEP 不为这种区别添加类型系统支持。如果将来缓冲区协议得到增强以提供静态内省支持,则可以重新审视这个问题。存在一个关于这种机制的草图。
致谢
许多人对本 PEP 的草案提供了有益的反馈。Petr Viktorin 在帮助我理解缓冲区协议的细微之处方面特别有帮助。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源: https://github.com/python/peps/blob/main/peps/pep-0688.rst
最后修改: 2025-03-05 16:28:34 GMT