PEP 579 – 重构 C 函数和方法
- 作者:
- Jeroen Demeyer <J.Demeyer at UGent.be>
- BDFL 委托:
- Petr Viktorin
- 状态:
- 最终版
- 类型:
- 信息性
- 创建日期:
- 2018-06-04
- 发布历史:
- 2018-06-20
批准通知
本 PEP 描述了 PEP 575、PEP 580、PEP 590(以及可能更晚的提案)中涉及的设计问题。
正如 PEP 1 中所述
信息性 PEP 不一定代表 Python 社区的共识或建议,因此用户和实现者可以自由地忽略信息性 PEP 或遵循其建议。
虽然对于本 PEP 中提出的问题或解决方案是否有效没有达成共识,但此列表仍然有助于指导未来的设计。
摘要
这个元 PEP 收集了 CPython 现有内置函数(用 C 实现的函数)和方法实现中的各种问题。
修复所有这些问题对于一个 PEP 来说太多了,因此将委托给其他标准追踪 PEP。然而,本 PEP 确实提供了一些可能的修复的简要思路。这主要旨在协调整体策略。例如,一个提出的解决方案可能对于修复任何单个问题来说听起来过于复杂,但它可能是解决多个问题的最佳整体解决方案。
本 PEP 纯粹是信息性的:它不意味着所有问题最终都会被修复,也不意味着它们会使用这里提出的解决方案来修复。
它还作为可能请求的功能的核对清单,以验证给定的修复不会使其他功能更难实现。
主要提议的更改是用新的结构 PyCCallDef 替换 PyMethodDef,该结构收集调用函数/方法所需的一切。在 PyTypeObject 结构中,添加了一个新的字段 tp_ccalloffset,它给出了对象结构中 PyCCallDef * 的偏移量。
注意:本 PEP 仅处理 CPython 实现细节,不影响 Python 语言或标准库。
问题
这列出了内置函数和方法的各种问题,以及解决方案计划和(如果适用)指向讨论细节的标准追踪 PEP 的指针。
1. 命名
“内置”一词在 Python 中被过度使用。快速浏览 Python 文档,它主要指 builtins 模块中的内容。换句话说:无需导入即可在全局命名空间中可用的内容。这与将“内置”一词用于表示“用 C 实现”的用法冲突。
解决方案:由于内置函数和方法的 C 结构已经称为 PyCFunctionObject,因此我们使用名称“cfunction”和“cmethod”而不是“内置函数”和“内置方法”。
2. 不可扩展
涉及的各种类(例如 builtin_function_or_method)不能被子类化
>>> from types import BuiltinFunctionType
>>> class X(BuiltinFunctionType):
... pass
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type 'builtin_function_or_method' is not an acceptable base type
这是一个问题,因为它使得无法向这些类添加内省支持等功能。
如果想用附加功能在 C 中实现一个函数,则必须从头开始实现一个全新的类。问题在于像 builtin_function_or_method 这样的现有类在 Python 解释器中进行了特殊处理,以允许更快的调用(例如,通过使用 METH_FASTCALL)。目前无法拥有具有相同优化的自定义类。
解决方案:使现有优化可用于任意类。这通过添加一个新的 PyTypeObject 字段 tp_ccalloffset(或者我们能为此重用 tp_print 吗?)来完成,该字段指定 PyCCallDef 指针的偏移量。这是一个新的结构,用于保存调用 cfunction 所需的所有信息,它将用于代替 PyMethodDef。这实现了新的“C 调用”协议。
对于构造 cfunction 和 cmethod,PyMethodDef 数组仍将使用(例如,在 tp_methods 中),但那将是 PyMethodDef 结构唯一剩下的用途。
此外,我们还可以使一些函数类可子类化。然而,一旦有了 tp_ccalloffset,这似乎就不那么重要了。
参考:PEP 580
3. C 函数不会变成方法
像 repr 这样的 cfunction 没有实现 __get__ 来绑定为方法
>>> class X:
... meth = repr
>>> x = X()
>>> x.meth()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: repr() takes exactly one argument (0 given)
在这个例子中,人们会期望 x.meth() 通过应用方法的正常规则返回 repr(x)。
这令人惊讶,并且是 cfunction 和 Python 函数之间不必要的区别。对于标准内置函数,这并不是一个真正的问题,因为它们不打算用作方法。但是,当想要实现一个新的 cfunction 以便可以用作方法时,这就会成为一个问题。
同样,一个解决方案是创建一个行为与 cfunction 完全相同但绑定为方法的新类。然而,这将失去一些现有的方法优化,例如 LOAD_METHOD/CALL_METHOD 操作码。
解决方案:与前一个问题相同。它只是表明处理 self 和 __get__ 应该是新 C 调用协议的一部分。
为了向后兼容,我们将保留 cfunction 现有的非绑定行为。我们只会在自定义类中允许它。
参考:PEP 580
4. inspect.isfunction 的语义
目前,inspect.isfunction 仅对 types.FunctionType 的实例返回 True。也就是说,真正的 Python 函数。
inspect.isfunction 的一个常见用例是检查内省:它例如保证 inspect.getfile() 将起作用。理想情况下,其他类也应该可以被视为函数。
解决方案:引入一个新的 InspectFunction 抽象基类,并用它来实现 inspect.isfunction。或者,对 inspect.isfunction 使用鸭子类型(如 [2] 中所建议)。
def isfunction(obj):
return hasattr(type(obj), "__code__")
5. C 函数应该能访问函数对象
cfunction 的底层 C 函数目前接受一个 self 参数(对于绑定方法),然后可能接受一些参数。C 函数无法实际访问 Python cfunction 对象(__call__ 或 tp_call 中的 self)。这例如允许为 Python 函数(types.FunctionType)实现 C 调用协议:实现调用 Python 函数的 C 函数需要访问函数的 __code__ 属性。
这也是 PEP 573 所必需的,其中所有 cfunction 都需要访问它们的“父级”(模块函数的模块或方法的定义类)。
解决方案:添加一个新的 PyMethodDef 标志来指定 C 函数接受一个额外参数(作为第一个参数),即函数对象。
6. METH_FASTCALL 是私有且未文档化的
METH_FASTCALL 机制允许使用 Python 对象的 C 数组而不是 tuple 来调用 cfunction 和 cmethod。这在 Python 3.6 中引入,仅用于位置参数,并在 Python 3.7 中扩展以支持关键字参数。
然而,鉴于它未被文档化,它大概只应由 CPython 本身使用。
解决方案:由于这是一个重要的优化,应该鼓励每个人使用它。既然 METH_FASTCALL 的实现已经稳定,请将其文档化!
作为 C 调用协议的一部分,我们还应该添加一个 C API 函数
PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *keywords)
参考:PEP 580
7. 允许原生 C 参数
cfunction 总是将其参数作为 Python 对象(例如,一个 PyObject 指针数组)。在 cfunction 真正包装原生 C 函数(例如,来自 ctypes 或像 Cython 这样的编译器)的情况下,这是低效的:C 代码到 C 代码的调用被迫使用 Python 对象来传递参数。
类似于允许访问 C 数据的缓冲区协议,我们也应该允许访问底层的 C 可调用对象。
解决方案:当将带有原生参数(例如,C long)的 C 函数包装到 cfunction 中时,我们也应该存储一个指向底层 C 函数的函数指针,以及它的 C 签名。
Argument Clinic 可以通过存储指向“impl”函数的指针来自动完成此操作。
8. 复杂性
有大量的类涉及实现方法的各种变体。这本身不是问题,但它是一个复合问题。
对于普通的 Python 类,下表给出了各种方法的类。列指的是类 __dict__ 中的类、未绑定方法(绑定到类)的类和绑定方法(绑定到实例)的类
| 种类 | __dict__ | 未绑定 | 已绑定 |
|---|---|---|---|
| 普通方法 | 函数 |
函数 |
方法 |
| 静态方法 | staticmethod |
函数 |
函数 |
| 类方法 | classmethod |
方法 |
方法 |
| 槽方法 | 函数 |
函数 |
方法 |
这是扩展类型(C 类)的类似表格
| 种类 | __dict__ | 未绑定 | 已绑定 |
|---|---|---|---|
| 普通方法 | method_descriptor |
method_descriptor |
builtin_function_or_method |
| 静态方法 | staticmethod |
builtin_function_or_method |
builtin_function_or_method |
| 类方法 | classmethod_descriptor |
builtin_function_or_method |
builtin_function_or_method |
| 槽方法 | wrapper_descriptor |
wrapper_descriptor |
method-wrapper |
涉及了很多类,这两个表看起来非常不同。Python 方法和 C 方法之间没有很好的理由进行根本性的区别对待。而且功能也略有不同:例如,method 支持 __func__,但 builtin_function_or_method 不支持。
由于 CPython 对这些对象的大多数调用都进行了优化,因此处理它们的代码也可能变得复杂。一个很好的例子是 Python/ceval.c 中的 call_function 函数。
解决方案:所有这些类都应该实现 C 调用协议。然后代码中的复杂性大多可以通过检查 C 调用协议(tp_ccalloffset != 0)而不是进行类型检查来解决。
此外,应该调查是否可以合并其中一些类,以及是否可以将 method 也用于扩展类型的绑定方法(后者参见 PEP 576,同时请记住这可能会有一些小的向后兼容性问题)。这本身不是一个目标,而是在处理这些类时需要记住的事情。
9. PyMethodDef 过于局限
在扩展模块中创建 cfunction 或 cmethod 的典型方法是使用 PyMethodDef 来定义它。这些然后存储在数组 PyModuleDef.m_methods(用于 cfunction)或 PyTypeObject.tp_methods(用于 cmethod)中。然而,由于稳定的 ABI(PEP 384),我们无法更改 PyMethodDef 结构。
因此,这意味着我们无法通过这种方式添加新字段来创建 cfunction/cmethod。这可能是 __doc__ 和 __text_signature__ 存储在同一个 C 字符串中(由 __doc__ 和 __text_signature__ 描述符提取相关部分)这种黑客行为的原因。
解决方案:停止假设单个 PyMethodDef 条目足以描述一个 cfunction/cmethod。相反,我们可以添加一些标志,这意味着其中一个 PyMethodDef 字段是指向附加结构的指针。或者,我们可以添加一个标志来使用数组中两个或更多连续的 PyMethodDef 条目来存储更多数据。然后 PyMethodDef 数组将仅用于构造 cfunction/cmethod,但此后将不再使用。
10. Slot 包装器没有自定义文档
目前,像 __init__ 或 __lt__ 这样的槽包装器只有非常通用的文档,根本不特定于该类
>>> list.__init__.__doc__
'Initialize self. See help(type(self)) for accurate signature.'
>>> list.__lt__.__doc__
'Return self<value.'
签名也发生同样的情况
>>> list.__init__.__text_signature__
'($self, /, *args, **kwargs)'
如您所见,槽包装器确实支持 __doc__ 和 __text_signature__。问题是这些存储在 struct wrapperbase 中,该结构对于特定槽的所有包装器都是通用的(例如,str.__eq__ 和 int.__eq__ 使用相同的 wrapperbase)。
解决方案:重新思考槽包装器类,以允许每个实例单独的 docstring(和文本签名)。
这仍然留下了扩展模块应如何指定文档的问题。PyTypeObject 条目如 tp_init 只是函数指针,我们无法对其进行任何操作。一个解决方案是向 tp_methods 数组添加条目,仅用于添加 docstring。这样的条目可能如下所示
{"__init__", NULL, METH_SLOTDOC, "pointer to __init__ doc goes here"}
11. 静态方法和类方法应可调用
staticmethod 和 classmethod 的实例应该可调用。诚然,这没有强烈的用例,但偶尔有人请求(例如参见 [1])。
使静态/类方法可调用将增加一致性。首先,函数装饰器通常添加功能或修改函数,但结果仍然可调用。对于 @staticmethod 和 @classmethod 则不是这样。
其次,扩展类型的类方法已经可调用
>>> fromhex = float.__dict__["fromhex"]
>>> type(fromhex)
<class 'classmethod_descriptor'>
>>> fromhex(float, "0xff")
255.0
第三,可以将 function、staticmethod 和 classmethod 视为不同种类的未绑定方法:它们在绑定时都变成 method,但 __get__ 的实现略有不同。从这个角度看,function 可调用而其他不可调用看起来很奇怪。
解决方案:在更改 staticmethod、classmethod 的实现时,我们应该考虑使实例可调用。即使这本身不是一个目标,它也可能由于实现而自然发生。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0579.rst