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

Python 增强提案

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 兼容性问题。这是一个 Python 优化间接被 C API 阻塞的具体示例。

此问题已在 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(例如,通过使用 HPyTupleBuilderHPyListBuilder)。

通过宏暴露的内部细节越少,HPy 提供直接等价物就越容易。任何引用“非公共”接口的宏都会有效地将这些接口公开暴露出来。

这些声明得到 Antonio Cuni(HPy 开发者)的认可。

GraalVM Python

在 GraalVM 中,当 Python C API 访问 Python 对象时,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 版本支持的情况下添加 Python 3.11 支持。该项目提供了一个头文件,为 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_namePyDescrObject.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_TYPEPy_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()

统计数据

总计(PyPI 和非 PyPI 上的项目),已知有 34 个项目受此 PEP 影响

  • 16 个项目(47%)已修复
  • 18 个项目(53%)尚未修复(待修复或必须重新生成其 Cython 代码)

2022 年 9 月 1 日,PEP 影响了 PyPI 前 5000 个项目中的 18 个项目(0.4%)

  • 15 个项目(0.3%)必须重新生成其 Cython 代码
  • 3 个项目(0.1%)有待修复

PyPI 前 5000

有待修复的项目 (3)

  • datatable (1.0.0):已修复
  • guppy3 (3.1.2):已修复
  • scipy (1.9.3):需要更新 boost python

此外,有 15 个项目必须重新生成其 Cython 代码。

已发布修复的项目 (12)

还有两个受此 PEP 影响的 backport 项目

  • pickle5 (0.0.12):Python <= 3.7 的 backport
  • pysha3 (1.0.2):Python <= 3.5 的 backport

它们不能在 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 仍然必须继续支持大量的 C 扩展,这些扩展不太可能很快移植到 HPy。

第二个担忧是无法演进 CPython 内部以实现新的优化,以及 PyPy、GraalPython 等中当前 C API 的低效实现。遗憾的是,HPy 只有在大多数 C 扩展完全移植到 HPy 后才能解决这些问题:当考虑放弃“遗留”Python C API 变得合理时。

虽然在 CPython 上可以逐步将 C 扩展移植到 HPy,但这需要修改大量代码并耗费时间。将大多数 C 扩展移植到 HPy 预计需要数年时间。

本 PEP 提议通过解决一个明确 identified 为导致实际问题的问题——用作左值的宏——来使 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 对象。

如果需要进行赋值,可以添加一个 setter 函数,并且宏文档可以要求使用 setter 函数。例如,Python 3.9 中添加了 Py_SET_TYPE() 函数,现在 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 现在会导致编译器错误,符合预期。

发布历史

参考资料

版本历史

  • 版本 3:不再更改 PyDescr_TYPE() 和 PyDescr_NAME() 宏
  • 版本 2:添加“与 HPy 项目的关系”部分,删除 PyPy 部分
  • 版本 1:第一个公开版本

来源:https://github.com/python/peps/blob/main/peps/pep-0674.rst

最后修改:2025-02-01 08:55:40 GMT