PEP 688 – 在 Python 中访问缓冲区协议
- 作者:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 讨论列表:
- Discourse 讨论帖
- 状态:
- 最终
- 类型:
- 标准跟踪
- 主题:
- 类型提示
- 创建:
- 2022-04-23
- Python 版本:
- 3.12
- 历史记录:
- 2022-04-23, 2022-04-25, 2022-10-06, 2022-10-26
- 决议:
- Discourse 消息
摘要
此 PEP 提出了一种用于缓冲区协议的 Python 级别 API,该协议目前仅对 C 代码可用。这允许类型检查器评估对象是否实现了该协议。
动机
CPython C API 提供了一种通用的机制来访问对象的底层内存——在 PEP 3118 中引入的 缓冲区协议。接受二进制数据的函数通常被编写为处理任何实现缓冲区协议的对象。例如,在撰写本文时,CPython 中大约有 130 个函数使用 Argument Clinic 的 Py_buffer
类型,该类型接受缓冲区协议。
目前,Python 代码无法检查对象是否支持缓冲区协议。此外,静态类型系统不提供表示该协议的类型注解。这是在为接受通用缓冲区的代码编写类型注解时遇到的一个 常见问题。
同样,用 Python 编写的类无法支持缓冲区协议。Python 中的缓冲区类将使用户能够轻松地包装 C 缓冲区对象,或测试使用缓冲区协议的 API 的行为。当然,这不是一个特别常见的需求。但是,自 2012 年以来,一直存在一个 CPython 功能请求,要求支持用 Python 编写的缓冲区类。
基本原理
当前方案
在类型系统中注释缓冲区类型有两种已知的解决方法,但都不够完善。
首先,typeshed 中 当前的解决方法 用于缓冲区类型,它是一个类型别名,列出了标准库中众所周知的缓冲区类型,例如 bytes
、bytearray
、memoryview
和 array.array
。这种方法适用于标准库,但它不适用于第三方缓冲区类型。
其次,typing.ByteString
的 文档 目前指出
此类型表示字节序列的类型bytes
、bytearray
和memoryview
。作为此类型的简写,
bytes
可用于注释上述任何类型的参数。
虽然这句话自 2015 年 就已出现在文档中,但使用 bytes
包含这些其他类型的行为在任何类型提示 PEP 中都没有说明。此外,此机制存在许多问题。它不包括所有可能的缓冲区类型,并且它使类型注解中的 bytes
类型变得模棱两可。毕竟,有很多操作对 bytes
对象有效,但对 memoryview
对象无效,并且一个函数完全可能接受 bytes
但不接受 memoryview
对象。一位 mypy 用户 报告说,此快捷方式给 psycopg
项目带来了重大问题。
缓冲区类型
C 缓冲区协议支持 许多选项,影响步长、连续性和对写入缓冲区的支持。其中一些选项在类型系统中将很有用。例如,typeshed 目前为可写和只读缓冲区提供了单独的类型别名。
但是,在 C 缓冲区协议中,大多数这些选项无法在类型对象上直接查询。确定对象是否支持特定标志的唯一方法是实际请求缓冲区。对于某些类型(例如 memoryview
),支持的标志取决于实例。因此,在类型系统中表示对这些标志的支持将很困难。
规范
Python 级别缓冲区协议
我们建议添加两个 Python 级别特殊方法,__buffer__
和 __release_buffer__
。实现这些方法的 Python 类可作为 C 代码中的缓冲区使用。反过来,用 C 实现并支持缓冲区协议的类将获得可从 Python 代码访问的合成方法。
例如,memoryview()
构造函数将调用 __buffer__
方法来从 Python 对象创建缓冲区。它对应于 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
赋予特殊含义
关于 bytes
可以用作其他 ByteString
类型的简写形式的特殊情况将从 typing
文档中删除。有了 collections.abc.Buffer
作为替代,就没有充分的理由允许 bytes
作为简写形式。目前实现此行为的类型检查器应弃用并最终删除它。
向后兼容性
__buffer__
和 __release_buffer__
属性
由于此 PEP 中的运行时更改仅添加了新功能,因此很少有向后兼容性问题。
但是,使用 __buffer__
或 __release_buffer__
属性用于其他目的的代码可能会受到影响。虽然所有 dunder 从技术上讲都是为语言保留的,但仍然建议确保新的 dunder 不会干扰太多现有代码,尤其是广泛使用的软件包。对公开可访问代码的调查发现
- PyPy 支持 与此 PEP 中提出的语义兼容的
__buffer__
方法。一位 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
提升,则使用它进行类型检查的几个主要的开源项目将看到新的错误。许多这些错误可以通过改进 typeshed 中的存根来修复,就像已经为 内置模块、binascii、pickle 和 re 模块所做的那样。正在对 typeshed 中 bytes
类型的所有用法进行 审查。总的来说,此更改提高了类型安全性并使类型系统更加一致,因此我们认为迁移成本是值得的。
如何讲解
我们将在文档的适当位置(例如 typing.readthedocs.io 和 mypy 速查表)中添加指向 collections.abc.Buffer
的注释。类型检查器可以在其错误消息中提供其他指针。例如,当它们遇到传递给仅注释为接受 bytes
的函数的缓冲区对象时,错误消息可能包含建议改为使用 collections.abc.Buffer
的注释。
参考实现
此 PEP 的实现 可在 作者的分支中获得。
被拒绝的想法
types.Buffer
此 PEP 的早期版本建议添加一个新的 types.Buffer
类型,并在 C 中实现 __instancecheck__
,以便可以使用 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
,如果只需要一组特定的方法)。这方面的提议在 键入签名邮件列表中进行了专门讨论,并且键入社区没有提出任何强烈反对意见。
区分可变和不可变缓冲区
缓冲区类型中最常用的区别是缓冲区是否可变。有些函数仅接受可变缓冲区(例如,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