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 兼容性问题。这是一个由 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(例如,使用 HPyTupleBuilderHPyListBuilder)。

通过宏公开的内部细节越少,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_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()

统计数据

总共有 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 个)

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

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

已发布修复版本的项目(12 个)

还有两个反向移植项目受此 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 现在会像预期的那样导致编译器错误。

历史记录

参考文献

版本历史

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

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

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