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(例如,通过使用 HPyTupleBuilder 或 HPyListBuilder)。
通过宏暴露的内部细节越少,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_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()。
统计数据
总计(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)
此外,有 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 影响的 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 现在会导致编译器错误,符合预期。
发布历史
- PEP 674 “禁止将宏用作左值”和 Python 3.11 (2022年8月18日)
- SC 对 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