PEP 579 – 重构 C 函数和方法
- 作者:
- Jeroen Demeyer <J.Demeyer at UGent.be>
- BDFL 代表:
- Petr Viktorin
- 状态:
- 最终
- 类型:
- 信息性
- 创建:
- 2018年6月4日
- 修订历史:
- 2018年6月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 调用”协议。
对于构造 cfunctions 和 cmethods,仍然会使用 PyMethodDef
数组(例如,在 tp_methods
中),但这将是 PyMethodDef
结构的唯一剩余用途。
此外,我们还可以使一些函数类可子类化。但是,一旦我们有了 tp_ccalloffset
,这似乎就不那么重要了。
参考:PEP 580
3. cfunctions 不会变成方法
像 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)
。
这令人惊讶,并且是 cfunctions 和 Python 函数之间不必要的区别。对于标准内置函数,这并不是真正的问题,因为这些函数并非旨在用作方法。但是,当要实现一个新的 cfunction 并希望将其用作方法时,它就会成为问题。
同样,一个解决方案可能是创建一个新的类,其行为与 cfunctions 完全相同,但可以绑定为方法。但是,这将失去某些方法的现有优化,例如 LOAD_METHOD
/CALL_METHOD
操作码。
解决方案:与上一个问题相同。它只是表明处理 self
和 __get__
应该成为新的 C 调用协议的一部分。
为了向后兼容,我们将保留 cfunctions 现有的非绑定行为。我们只需在自定义类中允许它。
参考: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 也是必需的,在该 PEP 中,所有 cfunctions 都需要访问其“父级”(模块的函数的模块或方法的定义类)。
解决方案:添加一个新的 PyMethodDef
标志以指定 C 函数采用一个附加参数(作为第一个参数),即函数对象。
6. METH_FASTCALL 是私有的且未文档化
METH_FASTCALL
机制允许使用 Python 对象的 C 数组而不是 tuple
来调用 cfunctions 和 cmethods。这在 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 可调用对象。
解决方案:当在 cfunction 内包装具有原生参数(例如,C long
)的 C 函数时,我们还应该存储指向底层 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. 插槽包装器没有自定义文档
现在,像 __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
中,该结构对于特定槽的所有包装器都是通用的(例如,相同的 wrapperbase
用于 str.__eq__
和 int.__eq__
)。
解决方案:重新考虑槽包装器类以允许为每个实例分别使用文档字符串(和文本签名)。
这仍然留下了扩展模块应该如何指定文档的问题。像 tp_init
这样的 PyTypeObject
条目只是函数指针,我们无法对它们进行任何操作。一个解决方案是向 tp_methods
数组中添加条目,仅用于添加文档字符串。这样的条目可能如下所示
{"__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
上次修改时间:2023-09-09 17:39:29 GMT