PEP 674 – 禁止将宏用作左值
- 作者:
- Victor Stinner <vstinner at python.org>
- 状态:
- 已延期
- 类型:
- 标准轨迹
- 创建:
- 2021年11月30日
- Python 版本:
- 3.12
摘要
禁止将宏用作左值。例如,Py_TYPE(obj) = new_type
现在会引发编译器错误。
在实践中,大多数受影响的项目只需要进行两次更改
- 将
Py_TYPE(obj) = new_type
替换为Py_SET_TYPE(obj, new_type)
。 - 将
Py_SIZE(obj) = new_size
替换为Py_SET_SIZE(obj, new_size)
。
PEP 延期
参见 SC 对 PEP 674 – 禁止将宏用作左值的回复(2022 年 2 月)。
基本原理
将宏用作左值
在 Python C API 中,某些函数被实现为宏,因为编写宏比编写常规函数更简单。如果宏直接公开结构成员,则从技术上讲,可以使用此宏不仅获取结构成员,还可以设置它。
Python 3.10 Py_TYPE()
宏示例
#define Py_TYPE(ob) (((PyObject *)(ob))->ob_type)
此宏可以用作 **右值** 来 **获取** 对象类型
type = Py_TYPE(object);
它也可以用作 **左值** 来 **设置** 对象类型
Py_TYPE(object) = new_type;
还可以使用 Py_REFCNT()
和 Py_SIZE()
宏设置对象引用计数和对象大小。
直接设置对象属性依赖于当前的精确 CPython 实现。在其他 Python 实现中实现此功能可能会降低其 C API 实现的效率。
CPython nogil 分支
Sam Gross 将 Python 3.9 分支以移除 GIL:nogil 分支。此分支没有 PyObject.ob_refcnt
成员,但具有更完善的引用计数实现,因此 Py_REFCNT(obj) = new_refcnt;
代码会引发编译器错误。
将 nogil 分支合并到上游 CPython 主分支需要首先修复此 C API 兼容性问题。这是一个由 C API 间接阻止的 Python 优化的具体示例。
此问题已在 Python 3.10 中修复:Py_REFCNT()
宏已修改为禁止将其用作左值。
这些声明得到 Sam Gross(nogil 开发人员)的支持。
HPy 项目
HPy 项目 是一个全新的 Python C API,仅使用句柄和函数调用:句柄是不透明的,无法直接访问结构成员,并且无法取消引用指针。
搜索和替换 Py_SET_SIZE()
比搜索和替换 Py_SIZE()
的一些奇怪的宏用法更容易且更安全。Py_SIZE()
可以半机械地替换为 HPy_Length()
,而看到 Py_SET_SIZE()
会立即清楚地表明代码需要进行更大的更改才能移植到 HPy(例如,使用 HPyTupleBuilder
或 HPyListBuilder
)。
通过宏公开的内部细节越少,HPy 提供直接等效项就越容易。任何引用“非公共”接口的宏实际上都将这些接口公开。
这些声明得到 Antonio Cuni(HPy 开发人员)的支持。
GraalVM Python
在 GraalVM 中,当 Python 对象被 Python C API 访问时,C API 模拟层必须将 GraalVM 对象包装到包装器中,这些包装器公开了 CPython 结构(PyObject、PyLongObject、PyTypeObject 等)的内部结构。这是因为当 C 代码直接或通过宏访问它时,GraalVM 可以拦截的只是结构偏移处的读取,这必须映射回 GraalVM 中的表示形式。“有效”公开的结构成员数量越少(通过用函数替换宏),GraalVM 包装器就越简单。
仅此 PEP 还不够,无法摆脱 GraalVM 中的包装器,但这是朝着这个长期目标迈出的一步。GraalVM 已经支持 HPy,从长远来看,这是一个更好的解决方案。
这些声明得到 Tim Felgentreff(GraalVM Python 开发人员)的支持。
规范
禁止将宏用作左值
以下 65 个宏已修改为禁止将其用作左值。
PyObject 和 PyVarObject 宏
Py_TYPE()
:必须改用Py_SET_TYPE()
Py_SIZE()
:必须改用Py_SET_SIZE()
GET 宏
PyByteArray_GET_SIZE()
PyBytes_GET_SIZE()
PyCFunction_GET_CLASS()
PyCFunction_GET_FLAGS()
PyCFunction_GET_FUNCTION()
PyCFunction_GET_SELF()
PyCell_GET()
PyCode_GetNumFree()
PyDict_GET_SIZE()
PyFunction_GET_ANNOTATIONS()
PyFunction_GET_CLOSURE()
PyFunction_GET_CODE()
PyFunction_GET_DEFAULTS()
PyFunction_GET_GLOBALS()
PyFunction_GET_KW_DEFAULTS()
PyFunction_GET_MODULE()
PyHeapType_GET_MEMBERS()
PyInstanceMethod_GET_FUNCTION()
PyList_GET_SIZE()
PyMemoryView_GET_BASE()
PyMemoryView_GET_BUFFER()
PyMethod_GET_FUNCTION()
PyMethod_GET_SELF()
PySet_GET_SIZE()
PyTuple_GET_SIZE()
PyUnicode_GET_DATA_SIZE()
PyUnicode_GET_LENGTH()
PyUnicode_GET_LENGTH()
PyUnicode_GET_SIZE()
PyWeakref_GET_OBJECT()
AS 宏
PyByteArray_AS_STRING()
PyBytes_AS_STRING()
PyFloat_AS_DOUBLE()
PyUnicode_AS_DATA()
PyUnicode_AS_UNICODE()
PyUnicode 宏
PyUnicode_1BYTE_DATA()
PyUnicode_2BYTE_DATA()
PyUnicode_4BYTE_DATA()
PyUnicode_DATA()
PyUnicode_IS_ASCII()
PyUnicode_IS_COMPACT()
PyUnicode_IS_READY()
PyUnicode_KIND()
PyUnicode_READ()
PyUnicode_READ_CHAR()
PyDateTime GET 宏
PyDateTime_DATE_GET_FOLD()
PyDateTime_DATE_GET_HOUR()
PyDateTime_DATE_GET_MICROSECOND()
PyDateTime_DATE_GET_MINUTE()
PyDateTime_DATE_GET_SECOND()
PyDateTime_DATE_GET_TZINFO()
PyDateTime_DELTA_GET_DAYS()
PyDateTime_DELTA_GET_MICROSECONDS()
PyDateTime_DELTA_GET_SECONDS()
PyDateTime_GET_DAY()
PyDateTime_GET_MONTH()
PyDateTime_GET_YEAR()
PyDateTime_TIME_GET_FOLD()
PyDateTime_TIME_GET_HOUR()
PyDateTime_TIME_GET_MICROSECOND()
PyDateTime_TIME_GET_MINUTE()
PyDateTime_TIME_GET_SECOND()
PyDateTime_TIME_GET_TZINFO()
将 C 扩展移植到 Python 3.11
在实践中,受这些 PEP 影响的大多数项目只需要进行两次更改
- 将
Py_TYPE(obj) = new_type
替换为Py_SET_TYPE(obj, new_type)
。 - 将
Py_SIZE(obj) = new_size
替换为Py_SET_SIZE(obj, new_size)
。
pythoncapi_compat 项目 可用于自动更新 C 扩展:添加 Python 3.11 支持,而不会失去对旧版 Python 版本的支持。该项目提供了一个头文件,该头文件为 Python 3.8 及更早版本提供 Py_SET_REFCNT()
、Py_SET_TYPE()
和 Py_SET_SIZE()
函数。
PyTuple_GET_ITEM() 和 PyList_GET_ITEM() 保持不变
PyTuple_GET_ITEM()
和 PyList_GET_ITEM()
宏保持不变。
代码模式 &PyTuple_GET_ITEM(tuple, 0)
和 &PyList_GET_ITEM(list, 0)
仍然常用于访问内部 PyObject**
数组。
更改这些宏不在本 PEP 的范围内。
PyDescr_NAME() 和 PyDescr_TYPE() 保持不变
PyDescr_NAME()
和 PyDescr_TYPE()
宏保持不变。
这些宏可访问 PyDescrObject.d_name
和 PyDescrObject.d_type
成员。它们可以用作左值来设置这些成员。
SWIG 项目将这些宏用作左值来设置这些成员。可以修改 SWIG 以防止直接设置 PyDescrObject
结构成员,但这并不值得,因为 PyDescrObject
结构不是性能关键的,并且不太可能很快发生变化。
有关更多详细信息,请参见 bpo-46538“[C API] 使 PyDescrObject 结构不透明:PyDescr_NAME() 和 PyDescr_TYPE()”问题。
实现
实现由 bpo-45476:[C API] PEP 674:禁止将宏用作左值 跟踪。
Py_TYPE() 和 Py_SIZE() 宏
2020 年 5 月,Py_TYPE()
和 Py_SIZE()
宏已修改为禁止将其用作左值(Py_TYPE、Py_SIZE)。
2020 年 11 月,更改被 撤消,因为它破坏了太多第三方项目。
2021 年 6 月,在大多数第三方项目更新后,进行了 第二次尝试,但不得不 再次撤消,因为它破坏了 Windows 上的 test_exceptions。
2021 年 9 月,在 test_exceptions 已修复 后,Py_TYPE() 和 Py_SIZE() 最终 更改。
2021年11月,此项向后不兼容的更改获得了指导委员会的例外。
2022年10月,Python 3.11 发布,其中包含 Py_TYPE() 和 Py_SIZE() 的不兼容更改。
向后兼容性
提议的 C API 更改故意向后不兼容。
实际上,只有 Py_TYPE()
和 Py_SIZE()
宏用作左值。
此更改不遵循 PEP 387 的弃用流程。没有已知的方法仅在宏用作左值时发出弃用警告,而在以其他方式使用时(例如:作为右值)不发出警告。
为了减少受影响项目的数量,以下 4 个宏保持不变:PyDescr_NAME()
、PyDescr_TYPE()
、PyList_GET_ITEM()
和 PyTuple_GET_ITEM()
。
统计数据
总共有 34 个项目(包括 PyPI 上和 PyPI 上的项目)受此 PEP 影响。
- 16 个项目(47%)已修复。
- 18 个项目(53%)尚未修复(待修复或必须重新生成其 Cython 代码)。
2022年9月1日,此 PEP 影响了前 5000 个 PyPI 项目中的 18 个项目(0.4%)。
- 15 个项目(0.3%)必须重新生成其 Cython 代码。
- 3 个项目(0.1%)待修复。
PyPI 前 5000 名
待修复的项目(3 个)
此外,还有 15 个项目必须重新生成其 Cython 代码。
已发布修复版本的项目(12 个)
- bitarray (1.6.2):提交
- Cython (0.29.20):提交
- immutables (0.15):提交
- mercurial (5.7):提交,错误报告
- mypy (v0.930):提交
- numpy (1.22.1):提交,提交 2
- pycurl (7.44.1):提交
- PyGObject (3.42.0)
- pyside2 (5.15.1):错误报告
- python-snappy (0.6.1):已修复
- recordclass (0.17.2):已修复
- zstd (1.5.0.3):提交
还有两个反向移植项目受此 PEP 影响。
- pickle5 (0.0.12):Python <= 3.7 的反向移植。
- pysha3 (1.0.2):Python <= 3.5 的反向移植。
它们不应在 Python 3.11 上使用,也无法使用。
其他受影响的项目
其他已发布修复版本的项目(4 个)
与 HPy 项目的关系
HPy 项目
HPy 项目希望提供一个接近原始 API 的 C API——以简化移植——并使其性能尽可能接近现有 API。同时,HPy 足够独立,可以成为一个良好的“C 扩展 API”(而不是 CPython 实现 API 的稳定子集),不会泄露实现细节。为了确保后一项属性,HPy 项目尝试为 CPython、PyPy 和 GraalVM Python 并行开发所有内容。
HPy 仍在快速发展。在迁移 NumPy 的过程中仍在解决问题,并且已开始为 Cython 添加对 HPy 的支持。对 pybind11 的工作即将开始。Tim Felgentreff 认为,当 HPy 让现有 C API 的这些用户能够工作时,HPy 应该处于一个普遍有用且足够稳定的状态,以便进一步的开发可以遵循更稳定的流程。
从长远来看,HPy 项目希望成为编写 Python C 扩展的推荐 API。
HPy 项目是长期解决方案的良好选择。它具有在 Python 之外开发的优势,并且不需要任何 C API 更改。
C API 将在未来几年内继续存在
关于 HPy 的第一个担忧是,目前,HPy 还没有成熟或广泛使用,CPython 仍然必须继续支持大量不太可能很快移植到 HPy 的 C 扩展。
第二个担忧是无法发展 CPython 内部以实现新的优化,以及当前 C API 在 PyPy、GraalPython 等中的低效实现。遗憾的是,只有当大多数 C 扩展完全移植到 HPy 时,HPy 才能解决这些问题:当可以合理地考虑放弃“遗留”Python C API 时。
虽然可以增量地在 CPython 上将 C 扩展移植到 HPy,但它需要修改大量代码并且需要时间。将大多数 C 扩展移植到 HPy 预计需要几年时间。
此 PEP 提出通过修复一个明确识别为导致实际问题的问题来使 C API“不那么糟糕”:用作左值的宏。此 PEP 只需要更新少数 C 扩展,并且通常只需要更改受影响扩展中的几行代码。
例如,NumPy 1.22 由 307,300 行 C 代码组成,并且使 NumPy 适应此 PEP 只修改了 11 行(使用 Py_SET_TYPE 和 Py_SET_SIZE)并添加了 4 行(为 Python 3.8 及更早版本定义 Py_SET_TYPE 和 Py_SET_SIZE)。NumPy 移植到 HPy 的初期已经需要修改比这更多的行。
现在,很难判断哪种方法是最好的:修复当前的 C API,还是专注于 HPy。只专注于 HPy 存在风险。
被拒绝的想法:保持宏不变
每个函数的文档可以劝阻开发人员使用宏来修改 Python 对象。
如果需要进行赋值,可以添加一个设置器函数,并且宏文档可以要求使用设置器函数。例如,Py_SET_TYPE()
函数已添加到 Python 3.9 中,并且 Py_TYPE()
文档现在要求使用 Py_SET_TYPE()
函数来设置对象类型。
如果开发人员将宏用作左值,那么当他们的代码中断时,这是他们的责任,而不是 Python 的责任。我们是在“双方自愿”原则下运作的:我们期望 Python C API 的用户按照文档使用它,并期望他们在出现问题时处理后果,如果他们在不遵循文档的情况下操作导致了问题。
这个想法被否决了,因为只有少数开发人员阅读文档,并且只有少数开发人员跟踪 Python C API 文档的更改。大多数开发人员只使用 CPython,因此不知道与其他 Python 实现的兼容性问题。
此外,继续允许将宏用作左值无助于 HPy 项目,并将模拟它们的负担留给了 GraalVM 的 Python 实现。
已修改的宏
以下 C API 宏已修改为不允许将其用作左值。
PyCell_SET()
PyList_SET_ITEM()
PyTuple_SET_ITEM()
Py_REFCNT()
(Python 3.10):必须使用Py_SET_REFCNT()
。_PyGCHead_SET_FINALIZED()
_PyGCHead_SET_NEXT()
asdl_seq_GET()
asdl_seq_GET_UNTYPED()
asdl_seq_LEN()
asdl_seq_SET()
asdl_seq_SET_UNTYPED()
例如,PyList_SET_ITEM(list, 0, item) < 0
现在会像预期的那样导致编译器错误。
历史记录
- PEP 674“禁止将宏用作左值”和 Python 3.11(2022 年 8 月 18 日)
- 指导委员会对 PEP 674 的回复——禁止将宏用作左值(2022 年 2 月 22 日)
- PEP 674:禁止将宏用作左值(版本 2)(2022 年 1 月 18 日)
- PEP 674:禁止将宏用作左值(2021 年 11 月 30 日)
参考文献
- Python C API:添加访问 PyObject 的函数(2021 年 10 月)Victor Stinner 的文章
- [capi-sig] Py_TYPE() 和 Py_SIZE() 成为静态内联函数(2021 年 9 月)
- [C API] 避免直接访问 PyObject 和 PyVarObject 成员:添加 Py_SET_TYPE() 和 Py_IS_TYPE(),禁止 Py_TYPE(obj)=type(2020 年 2 月)
- bpo-30459:PyList_SET_ITEM 可以更安全(2017 年 5 月)
版本历史
- 版本 3:不再更改 PyDescr_TYPE() 和 PyDescr_NAME() 宏。
- 版本 2:添加“与 HPy 项目的关系”部分,删除 PyPy 部分。
- 版本 1:第一个公开版本。
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可证,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0674.rst
上次修改时间:2023-09-09 17:39:29 GMT