参数诊所

作者:

Larry Hastings

源代码: Tools/clinic/clinic.py.

参数诊所是 CPython C 文件的预处理器。它在 Python 3.4 中引入,带有 PEP 436,以便提供自省签名,并为 CPython 内置函数、模块级函数和类方法中的参数解析生成高性能且定制的样板代码。本文档分为四个主要部分

  • 背景讨论了参数诊所的基本概念和目标。

  • 参考描述了命令行界面和参数诊所术语。

  • 教程指导你完成将现有 C 函数调整为参数诊所所需的所有步骤。

  • 操作指南详细说明如何处理特定任务。

注意

参数诊所被认为仅供 CPython 内部使用。不支持在 CPython 外部文件使用它,并且不保证未来版本的向后兼容性。换句话说:如果你维护 CPython 的外部 C 扩展,欢迎你在自己的代码中尝试参数诊所。但是,随 CPython 下一版本一起提供的参数诊所版本可能完全不兼容并破坏你的所有代码。

背景

基本概念

当参数诊所通过 命令行界面或通过 make clinic 在文件中运行时,它将扫描输入文件,查找 开始行

/*[clinic input]

当它找到一个时,它会读取所有内容,直到 结束行

[clinic start generated code]*/

这两行之间的所有内容都是参数诊所 输入。当参数诊所解析输入时,它会生成 输出。输出在输入之后立即重写到 C 文件中,后面跟着 校验和行。所有这些行,包括 开始行校验和行,统称为参数诊所

/*[clinic input]
... clinic input goes here ...
[clinic start generated code]*/
... clinic output goes here ...
/*[clinic end generated code: ...]*/

如果你在同一文件中第二次运行参数诊所,参数诊所将丢弃旧 输出,并使用新的 校验和行写出新输出。如果 输入 没有改变,那么输出也不会改变。

注意

您永远不应该修改 Argument Clinic 块的输出,因为 Argument Clinic 运行时任何更改都会丢失;Argument Clinic 将检测到输出校验和不匹配并重新生成正确的输出。如果您对生成的输出不满意,您应该更改输入,直到它生成您想要的输出。

参考

术语

开始行

/*[clinic input]。此行标记 Argument Clinic 输入的开始。请注意,开始行打开 C 块注释。

结束行

[clinic start generated code]*/结束行标记 Argument Clinic 输入的_结束_,但同时标记 Argument Clinic 输出的_开始_,因此文本“clinic start start generated code”请注意,结束行关闭了由开始行打开的 C 块注释。

校验和

哈希,用于区分唯一的 输入输出

校验和行

类似于 /*[clinic end generated code: ...]*/ 的行。三个点将被 输入生成的 校验和输出生成的 校验和 替换。校验和行标记 Argument Clinic 生成的代码的结束,Argument Clinic 使用它来确定是否需要重新生成输出。

输入

介于 开始行结束行 之间的文本。请注意,开始行和结束行打开和关闭 C 块注释;因此,输入是该 C 块注释的一部分。

输出

介于 结束行校验和行 之间的文本。

开始行校验和行 的所有文本,包括两者。

命令行界面

Argument Clinic CLI 通常用于处理单个源文件,如下所示

$ python3 ./Tools/clinic/clinic.py foo.c

CLI 支持以下选项

-h, --help

打印 CLI 用法。

-f, --force

强制输出重新生成。

-o, --output OUTPUT

将文件输出重定向到 OUTPUT

-v, --verbose

启用详细模式。

--converters

打印所有受支持转换器的列表并返回转换器。

--make

遍历 --srcdir 以遍历所有相关文件。

--srcdir SRCDIR

--make 模式中遍历的目录树。

--exclude EXCLUDE

--make 模式中排除的文件。此选项可以多次给出。

--limited

使用 受限 API 解析生成 C 代码中的参数。请参阅 如何使用受限 C API

FILE ...

要处理的文件列表。

用于扩展 Argument Clinic 的类

clinic.CConverter

所有转换器的基类。有关如何对该类进行子类化,请参阅如何编写自定义转换器

类型

用于此变量的 C 类型。type 应为指定类型的 Python 字符串,例如 'int'。如果这是指针类型,则类型字符串应以 ' *' 结尾。

默认

此参数的 Python 默认值,作为 Python 值。如果没有默认值,则为魔术值 unspecified

py_default

default 应在 Python 代码中显示为字符串。如果没有默认值,则为 None

c_default

default 应在 C 代码中显示为字符串。如果没有默认值,则为 None

c_ignored_default

当没有默认值时用于初始化 C 变量的默认值,但未指定默认值可能会导致“未初始化变量”警告。在使用选项组时很容易发生这种情况——尽管编写正确的代码永远不会实际使用此值,但变量确实会传递到 impl 中,C 编译器会抱怨未初始化值的“使用”。此值应始终为非空字符串。

转换器

C 转换器函数的名称,作为字符串。

impl_by_reference

布尔值。如果为真,Argument Clinic 将在将变量传递到 impl 函数时在其名称前添加 &

parse_by_reference

布尔值。如果为真,Argument Clinic 将在将变量传递到 PyArg_ParseTuple() 时在其名称前添加 &

教程

了解 Argument Clinic 工作原理的最佳方法是转换一个函数以使其与之配合使用。因此,以下是将函数转换为与 Argument Clinic 配合使用的最低步骤。请注意,对于你计划签入 CPython 的代码,你确实应该进一步转换,使用一些高级概念,你将在本文档后面看到,例如如何使用返回转换器如何使用“self 转换器”。但我们将保持此演练的简单性,以便你学习。

首先,确保你使用的是 CPython 主干的新鲜更新签出。

接下来,找到一个调用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 的 Python 内置函数,并且尚未转换为使用 Argument Clinic。在本教程中,我们将使用 _pickle.Pickler.dump

如果对 PyArg_Parse*() 函数的调用使用了以下任何格式单元…

O&
O!
es
es#
et
et#

…或者如果它对 PyArg_ParseTuple() 有多个调用,则应选择不同的函数。(有关这些场景,请参见 如何使用高级转换器。)

此外,如果函数对 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 有多个调用,其中它支持相同参数的不同类型,或者如果函数使用 PyArg_Parse*() 函数之外的其他内容来解析其参数,则可能不适合转换为 Argument Clinic。Argument Clinic 不支持泛型函数或多态参数。

接下来,在函数上方添加以下样板,创建我们的输入块

/*[clinic input]
[clinic start generated code]*/

剪切文档字符串并将其粘贴在 [clinic] 行之间,删除所有使其成为正确引用的 C 字符串的垃圾。完成后,您应该只有文本,基于左页边距,没有比 80 个字符更宽的行。Argument Clinic 将保留文档字符串中的缩进。

如果旧的文档字符串的第一行看起来像函数签名,请丢弃该行;文档字符串不再需要它——当您将来对内置函数使用 help() 时,第一行将根据函数签名自动生成。

示例文档字符串摘要行

/*[clinic input]
Write a pickled representation of obj to the open file.
[clinic start generated code]*/

如果您的文档字符串没有“摘要”行,Argument Clinic 将会抱怨,所以让我们确保它有一个。“摘要”行应为一个段落,由文档字符串开头处的单行 80 列组成。(有关文档字符串约定,请参见 PEP 257。)

我们的示例文档字符串仅包含摘要行,因此示例代码不必为此步骤进行更改。

现在,在文档字符串上方输入函数名称,后跟一个空行。这应该是函数的 Python 名称,并且应该是指向函数的完整点分路径——它应以模块名称开头,包括任何子模块,如果函数是类上的方法,则还应包括类名称。

在我们的示例中,_pickle 是模块,Pickler 是类,dump() 是方法,因此名称变为 _pickle.Pickler.dump()

/*[clinic input]
_pickle.Pickler.dump

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

如果这是该模块或类在该 C 文件中首次与 Argument Clinic 一起使用,则必须声明该模块和/或类。适当的 Argument Clinic 卫生习惯更喜欢在 C 文件顶部附近的某个单独块中声明这些内容,就像包含文件和静态内容在顶部一样。在我们的示例代码中,我们将只显示彼此相邻的两个块。

类和模块的名称应与 Python 看到的名称相同。检查 PyModuleDefPyTypeObject 中定义的名称(视情况而定)。

声明类时,还必须在 C 中指定其类型的两个方面:用于指向此类的实例的指针的类型声明,以及指向此类的 PyTypeObject 的指针

/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
[clinic start generated code]*/

/*[clinic input]
_pickle.Pickler.dump

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

声明函数的每个参数。每个参数应获取其自己的行。所有参数行应从函数名称和文档字符串缩进。这些参数行的常规形式如下

name_of_parameter: converter

如果参数具有默认值,请在转换器后添加该值

name_of_parameter: converter = default_value

Argument Clinic 对“默认值”的支持非常复杂;请参阅 如何将默认值分配给参数 了解更多信息。

接下来,在参数下方添加一个空行。

什么是“转换器”?它既建立了 C 中使用的变量类型,又建立了在运行时将 Python 值转换为 C 值的方法。现在,您将使用所谓的“旧版转换器”——一种方便的语法,旨在使将旧代码移植到 Argument Clinic 中变得更容易。

对于每个参数,从 PyArg_Parse() 格式参数中复制该参数的“格式单元”,并将其指定为其转换器,作为带引号的字符串。“格式单元”是格式参数中一到三个字符子字符串的正式名称,它告诉参数解析函数变量的类型以及如何转换它。有关格式单元的更多信息,请参阅 解析参数和构建值

对于 z# 等多字符格式单元,请使用整个两个或三个字符的字符串。

示例

/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
[clinic start generated code]*/

/*[clinic input]
_pickle.Pickler.dump

    obj: 'O'

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

如果您的函数在格式字符串中具有 |,表示某些参数具有默认值,则可以忽略它。Argument Clinic 根据参数是否有默认值来推断哪些参数是可选的。

如果您的函数在格式字符串中具有 $,表示它采用仅关键字参数,请在第一个仅关键字参数之前指定 *,缩进与参数行相同。

_pickle.Pickler.dump() 没有,因此我们的示例保持不变。

接下来,如果现有的 C 函数调用 PyArg_ParseTuple()(而不是 PyArg_ParseTupleAndKeywords()),则其所有参数都是仅位置参数。

要在 Argument Clinic 中将参数标记为仅位置参数,请在最后一个仅位置参数后添加一个 /,缩进与参数行相同。

示例

/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
[clinic start generated code]*/

/*[clinic input]
_pickle.Pickler.dump

    obj: 'O'
    /

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

为每个参数编写每个参数的文档字符串可能会有所帮助。由于每个参数的文档字符串是可选的,因此如果您愿意,可以跳过此步骤。

不过,以下是如何添加每个参数的文档字符串。每个参数文档字符串的第一行必须比参数定义缩进更多。此第一行的左边界建立了整个每个参数文档字符串的左边界;您编写的全部文本将按此量缩进。您可以根据需要跨多行编写任意数量的文本。

示例

/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
[clinic start generated code]*/

/*[clinic input]
_pickle.Pickler.dump

    obj: 'O'
        The object to be pickled.
    /

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

保存并关闭文件,然后在其上运行 Tools/clinic/clinic.py。如果幸运的话,一切都奏效了——您的块现在有输出了,并且已经生成了一个 .c.h 文件!在文本编辑器中重新加载文件以查看生成的代码

/*[clinic input]
_pickle.Pickler.dump

    obj: 'O'
        The object to be pickled.
    /

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

static PyObject *
_pickle_Pickler_dump(PicklerObject *self, PyObject *obj)
/*[clinic end generated code: output=87ecad1261e02ac7 input=552eb1c0f52260d9]*/

显然,如果 Argument Clinic 没有产生任何输出,那是因为它在您的输入中发现了错误。请继续修复错误并重试,直到 Argument Clinic 无错误地处理您的文件。

为了可读性,大多数胶合代码已生成到 .c.h 文件中。您需要将其包含在原始 .c 文件中,通常紧跟在 clinic 模块块之后

#include "clinic/_pickle.c.h"

仔细检查 Argument Clinic 生成的参数解析代码是否与现有代码基本相同。

首先,确保两个地方都使用相同参数解析函数。现有代码必须调用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords();确保 Argument Clinic 生成的代码调用完全相同的函数。

其次,传递给 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 的格式字符串应完全与现有函数中的手动编写的格式字符串相同,直到冒号或分号。

Argument Clinic 始终使用 : 加上函数名称生成其格式字符串。如果现有代码的格式字符串以 ; 结尾,以提供使用帮助,此更改是无害的 - 不用担心。

第三,对于格式单元需要两个参数的参数,例如长度变量、编码字符串或指向转换函数的指针,请确保两次调用之间的第二个参数完全相同。

第四,在块的输出部分中,您会找到一个预处理程序宏,该宏定义了此内置函数的适当静态 PyMethodDef 结构

#define __PICKLE_PICKLER_DUMP_METHODDEF    \
{"dump", (PyCFunction)__pickle_Pickler_dump, METH_O, __pickle_Pickler_dump__doc__},

此静态结构应完全与此内置函数的现有静态 PyMethodDef 结构相同。

如果其中任何一项以任何方式不同,请调整您的 Argument Clinic 函数规范并重新运行 Tools/clinic/clinic.py,直到它们相同为止。

请注意,其输出的最后一行是您的“impl”函数的声明。这是内置函数的实现所在的位置。删除您要修改的函数的现有原型,但保留左花括号。现在删除其参数解析代码和它将参数转储到的所有变量的声明。请注意,Python 参数现在是此 impl 函数的参数;如果实现为这些变量使用了不同的名称,请修复它。

让我们重申一下,因为它有点奇怪。您的代码现在应该如下所示

static return_type
your_function_impl(...)
/*[clinic end generated code: input=..., output=...]*/
{
...

Argument Clinic 生成了校验和行和它正上方的函数原型。您应该为函数编写左花括号和右花括号,以及内部实现。

示例

/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
[clinic start generated code]*/
/*[clinic end generated code: checksum=da39a3ee5e6b4b0d3255bfef95601890afd80709]*/

/*[clinic input]
_pickle.Pickler.dump

    obj: 'O'
        The object to be pickled.
    /

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

PyDoc_STRVAR(__pickle_Pickler_dump__doc__,
"Write a pickled representation of obj to the open file.\n"
"\n"
...
static PyObject *
_pickle_Pickler_dump_impl(PicklerObject *self, PyObject *obj)
/*[clinic end generated code: checksum=3bd30745bf206a48f8b576a1da3d90f55a0a4187]*/
{
    /* Check whether the Pickler was initialized correctly (issue3664).
       Developers often forget to call __init__() in their subclasses, which
       would trigger a segfault without this check. */
    if (self->write == NULL) {
        PyErr_Format(PicklingError,
                     "Pickler.__init__() was not called by %s.__init__()",
                     Py_TYPE(self)->tp_name);
        return NULL;
    }

    if (_Pickler_ClearBuffer(self) < 0) {
        return NULL;
    }

    ...

还记得带有 PyMethodDef 结构的宏吗?找到此函数的现有 PyMethodDef 结构,并用对该宏的引用替换它。如果内置函数在模块作用域中,这可能非常接近文件末尾;如果内置函数是类方法,这可能在实现的下方但相对接近实现。

请注意,宏的主体包含一个尾随逗号;当您用宏替换现有的静态 PyMethodDef 结构时,不要在末尾添加逗号。

示例

static struct PyMethodDef Pickler_methods[] = {
    __PICKLE_PICKLER_DUMP_METHODDEF
    __PICKLE_PICKLER_CLEAR_MEMO_METHODDEF
    {NULL, NULL}                /* sentinel */
};

Argument Clinic 可能会生成 _Py_ID 的新实例。例如

&_Py_ID(new_unique_py_id)

如果确实如此,您必须运行 make regen-global-objects 以在此处重新生成预编译标识符列表。

最后,编译,然后运行回归测试套件的相关部分。此更改不应引入任何新的编译时警告或错误,并且除了一个区别之外,Python 的行为也不应有任何外部可见的变化:inspect.signature()现在应该在你的函数上运行并提供一个有效的签名!

恭喜,你已经将你的第一个函数移植到 Argument Clinic 中工作!

操作指南

如何重命名 Argument Clinic 生成的 C 函数和变量

Argument Clinic 会自动命名它为你生成的函数。有时这可能会导致问题,如果生成的名称与现有 C 函数的名称冲突。有一个简单的解决方案:覆盖用于 C 函数的名称。只需在你的函数声明行中添加关键字 "as",后跟你要使用的函数名称。Argument Clinic 将使用该函数名称作为基本(生成)函数,然后在末尾添加 "_impl" 并将其用作 impl 函数的名称。

例如,如果我们想重命名为 pickle.Pickler.dump() 生成的 C 函数名称,它将如下所示

/*[clinic input]
pickle.Pickler.dump as pickler_dumper

...

基本函数现在将被命名为 pickler_dumper(),而 impl 函数现在将被命名为 pickler_dumper_impl()

类似地,你可能遇到一个问题,即你想给一个参数一个特定的 Python 名称,但该名称在 C 中可能不方便。Argument Clinic 允许你使用相同的 "as" 语法在 Python 和 C 中为参数指定不同的名称

/*[clinic input]
pickle.Pickler.dump

    obj: object
    file as file_obj: object
    protocol: object = NULL
    *
    fix_imports: bool = True

在这里,在 Python 中使用的名称(在签名和 keywords 数组中)将是 file,但 C 变量将被命名为 file_obj

你也可以用它来重命名 self 参数!

如何使用 PyArg_UnpackTuple 转换函数

要转换使用 PyArg_UnpackTuple() 解析其参数的函数,只需写出所有参数,将每个参数指定为 object。你可以指定 type 参数以适当地强制转换类型。所有参数都应标记为仅限位置(在最后一个参数后单独一行添加 /)。

目前,生成的代码将使用 PyArg_ParseTuple(),但很快就会更改。

如何使用可选组

一些旧功能在解析其参数时采用了一种棘手的方法:它们计算位置参数的数量,然后使用 switch 语句调用多个不同的 PyArg_ParseTuple() 调用,具体取决于位置参数的数量。(这些函数不能接受仅关键字参数。)这种方法用于在 PyArg_ParseTupleAndKeywords() 创建之前模拟可选参数。

虽然使用这种方法的函数通常可以转换为使用 PyArg_ParseTupleAndKeywords()、可选参数和默认值,但并非总是可行。其中一些旧功能具有 PyArg_ParseTupleAndKeywords() 不直接支持的行为。最明显的例子是内置函数 range(),它在其必需参数的左侧有一个可选参数!另一个示例是 curses.window.addch(),它有一组必须始终一起指定的两个参数。(这些参数称为xy;如果你调用函数传递x,你也必须传递y——如果你不传递x,你也不能传递y。)

在任何情况下,Argument Clinic 的目标都是支持解析所有现有 CPython 内置函数的参数,而无需更改其语义。因此,Argument Clinic 支持这种使用可选组的替代解析方法。可选组是必须一起传递的参数组。它们可以位于必需参数的左侧或右侧。它们只能与仅位置参数一起使用。

注意

可选组适用于转换对 PyArg_ParseTuple() 进行多次调用的函数!使用任何其他参数解析方法的函数几乎永远不应使用可选组转换为 Argument Clinic。使用可选组的函数当前无法在 Python 中具有准确的签名,因为 Python 根本不理解这个概念。请尽可能避免使用可选组。

要指定可选组,请在要组合在一起的参数之前单独一行添加 [,并在这些参数之后单独一行添加 ]。例如,以下是如何 curses.window.addch() 使用可选组使前两个参数和最后一个参数变为可选

/*[clinic input]

curses.window.addch

    [
    x: int
      X-coordinate.
    y: int
      Y-coordinate.
    ]

    ch: object
      Character to add.

    [
    attr: long
      Attributes for the character.
    ]
    /

...

注释

  • 对于每个可选组,将向表示该组的 impl 函数传递一个附加参数。该参数将是一个名为 group_{direction}_{number} 的 int,其中 {direction}rightleft,具体取决于该组是在必需参数之前还是之后,而 {number} 是一个单调递增的数字(从 1 开始),表示该组与必需参数的距离。当调用 impl 时,如果该组未使用,则此参数将设置为零,如果该组已使用,则设置为非零。(我所说的使用或未使用是指在该调用中参数是否接收了参数。)

  • 如果没有必需参数,可选组将表现得好像它们在必需参数的右侧。

  • 在模棱两可的情况下,参数解析代码优先考虑左侧(必需参数之前)的参数。

  • 可选组只能包含仅位置的参数。

  • 可选组适用于旧代码。请不要将可选组用于新代码。

如何使用真正的 Argument Clinic 转换器,而不是“旧转换器”

为了节省时间并最大程度地减少您需要学习以实现首次移植到 Argument Clinic 的内容,上面的演练告诉您使用“旧转换器”。“旧转换器”是一种便利,明确设计为使将现有代码移植到 Argument Clinic 变得更加容易。

但是,从长远来看,我们可能希望所有块都使用 Argument Clinic 的真实转换器语法。为什么?有几个原因

  • 正确的转换器更容易阅读,并且其意图更清晰。

  • 有一些格式单元不受支持作为“旧转换器”,因为它们需要参数,而旧转换器语法不支持指定参数。

  • 将来,我们可能会有一个新的参数解析库,它不受 PyArg_ParseTuple() 支持的内容的限制;使用旧转换器的参数将无法使用此灵活性。

因此,如果您不介意付出一些额外的努力,请使用普通转换器,而不是旧转换器。

简而言之,Argument Clinic(非旧版)转换器的语法看起来像 Python 函数调用。但是,如果函数没有显式参数(所有函数都采用其默认值),则可以省略括号。因此,boolbool() 是完全相同的转换器。

Argument Clinic 转换器的所有参数都是仅关键字的。所有 Argument Clinic 转换器都接受以下参数

c_default

在 C 中定义此参数时的默认值。具体来说,这将是“解析函数”中声明的变量的初始化程序。有关如何使用此功能,请参阅 有关默认值的部分。指定为字符串。

annotation

此参数的注释值。目前不受支持,因为 PEP 8 要求 Python 库不得使用注释。

unused

使用 Py_UNUSED 在 impl 函数签名中包装参数。

此外,一些转换器接受其他参数。以下是这些参数及其含义的列表

accept

一组 Python 类型(可能还有伪类型);这将允许的 Python 参数限制为这些类型的值。(这不是一个通用工具;通常,它只支持旧转换器表中所示的特定类型列表。)

要接受 None,请将 NoneType 添加到此集合。

按位

仅支持无符号整数。此 Python 参数的本机整数值将写入参数,而无需任何范围检查,即使对于负值也是如此。

转换器

仅受 object 转换器支持。指定 C “转换函数” 的名称,用于将此对象转换为本机类型。

编码

仅支持字符串。指定在将此字符串从 Python str(Unicode)值转换为 C char * 值时要使用的编码。

子类

仅受 object 转换器支持。要求 Python 值是 Python 类型的子类,如 C 中所表达。

类型

仅支持 objectself 转换器。指定将用于声明变量的 C 类型。默认值为 "PyObject *"

仅支持字符串。如果为真,则允许在值中嵌入 NUL 字节 ('\\0')。字符串的长度将作为名为 <parameter_name>_length 的参数传递到 impl 函数中,紧跟在字符串参数之后。

请注意,并非所有可能的参数组合都能正常工作。通常,这些参数由特定的 PyArg_ParseTuple() 格式单元 实现,具有特定的行为。例如,目前你无法在不指定 bitwise=True 的情况下调用 unsigned_short。尽管认为这会奏效是完全合理的,但这些语义不会映射到任何现有的格式单元。因此,Argument Clinic 不支持它。(或者,至少目前还不支持。)

下表显示了将旧转换器映射到真正的 Argument Clinic 转换器的映射。左侧是旧转换器,右侧是替换它的文本。

'B'

unsigned_char(bitwise=True)

'b'

unsigned_char

'c'

char

'C'

int(accept={str})

'd'

double

'D'

Py_complex

'es'

str(encoding='name_of_encoding')

'es#'

str(encoding='name_of_encoding', zeroes=True)

'et'

str(encoding='name_of_encoding', accept={bytes, bytearray, str})

'et#'

str(encoding='name_of_encoding', accept={bytes, bytearray, str}, zeroes=True)

'f'

float

'h'

short

'H'

unsigned_short(bitwise=True)

'i'

int

'I'

unsigned_int(bitwise=True)

'k'

unsigned_long(bitwise=True)

'K'

unsigned_long_long(bitwise=True)

'l'

long

'L'

long long

'n'

Py_ssize_t

'O'

对象

'O!'

object(subclass_of='&PySomething_Type')

'O&'

object(converter='name_of_c_function')

'p'

布尔值

'S'

PyBytesObject

's'

str

's#'

str(zeroes=True)

's*'

Py_buffer(accept={buffer, str})

'U'

unicode

'u'

wchar_t

'u#'

wchar_t(zeroes=True)

'w*'

Py_buffer(accept={rwbuffer})

'Y'

PyByteArrayObject

'y'

str(accept={bytes})

'y#'

str(accept={robuffer}, zeroes=True)

'y*'

Py_buffer

'Z'

wchar_t(accept={str, NoneType})

'Z#'

wchar_t(accept={str, NoneType}, zeroes=True)

'z'

str(accept={str, NoneType})

'z#'

str(accept={str, NoneType}, zeroes=True)

'z*'

Py_buffer(accept={buffer, str, NoneType})

例如,以下是使用正确转换器的示例 pickle.Pickler.dump

/*[clinic input]
pickle.Pickler.dump

    obj: object
        The object to be pickled.
    /

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

真实转换器的一个优点是它们比传统转换器更灵活。例如,unsigned_int 转换器(以及所有 unsigned_ 转换器)可以在没有 bitwise=True 的情况下指定。它们的默认行为是对值执行范围检查,并且不接受负数。使用传统转换器就无法做到这一点!

Argument Clinic 将向你展示它所拥有的所有转换器。对于每个转换器,它将向你展示它接受的所有参数,以及每个参数的默认值。只需运行 Tools/clinic/clinic.py --converters 即可查看完整列表。

如何使用 Py_buffer 转换器

使用 Py_buffer 转换器(或 's*''w*''*y''z*' 传统转换器)时,不得对提供的缓冲区调用 PyBuffer_Release()。Argument Clinic 生成了为你执行此操作的代码(在解析函数中)。

如何使用高级转换器

还记得那些你第一次跳过的格式单元吗?因为它们是高级的?以下是如何处理这些格式单元。

诀窍在于,所有这些格式单元都接受参数——转换函数、类型或指定编码的字符串。(但“旧版转换器”不支持参数。这就是我们为你的第一个函数跳过它们的原因。)你为格式单元指定的参数现在是转换器的参数;此参数可能是 converter(对于 O&)、subclass_of(对于 O!)或 encoding(对于所有以 e 开头的格式单元)。

在使用 subclass_of 时,你可能还想为 object() 使用另一个自定义参数:type,它允许你设置实际用于参数的类型。例如,如果你想确保对象是 PyUnicode_Type 的子类,你可能想使用转换器 object(type='PyUnicodeObject *', subclass_of='&PyUnicode_Type')

使用 Argument Clinic 的一个可能问题是:它取消了以 e 开头的格式单元的一些可能的灵活性。在手动编写 PyArg_Parse*() 调用时,你理论上可以在运行时决定将哪个编码字符串传递给该调用。但现在,此字符串必须在 Argument-Clinic 预处理时间硬编码。此限制是故意的;它使支持此格式单元变得更加容易,并且可能允许进行未来的优化。此限制似乎并不过分;对于格式单元以 e 开头的参数,CPython 本身始终传入静态硬编码编码字符串。

如何为参数分配默认值

参数的默认值可以是许多值中的任何一个。最简单的情况是,它们可以是字符串、int 或 float 文字

foo: str = "abc"
bar: int = 123
bat: float = 45.6

它们还可以使用任何 Python 内置常量

yep:  bool = True
nope: bool = False
nada: object = None

还特别支持默认值为 NULL,以及在以下部分中记录的简单表达式。

NULL 默认值

对于字符串和对象参数,你可以将它们设置为 None 以表明没有默认值。但是,这意味着 C 变量将被初始化为 Py_None。为了方便起见,有一个特殊值称为 NULL,原因正是如此:从 Python 的角度来看,它表现得像 None 的默认值,但 C 变量用 NULL 初始化。

符号默认值

你为参数提供的默认值不能是任何任意表达式。目前明确支持以下内容

  • 数字常量(整数和小数)

  • 字符串常量

  • TrueFalseNone

  • 以模块名称开头的简单符号常量,如 sys.maxsize

(将来,这可能需要变得更加复杂,以允许像 CONSTANT - 1 这样的完整表达式。)

表达式作为默认值

参数的默认值不仅仅可以是字面值。它可以是整个表达式,使用数学运算符并在对象上查找属性。但是,由于一些不明显的语义,这种支持并不简单。

考虑以下示例

foo: Py_ssize_t = sys.maxsize - 1

sys.maxsize 在不同的平台上可以有不同的值。因此,Argument Clinic 无法简单地在本地计算该表达式并将其硬编码为 C。因此,它以一种方式存储默认值,以便在用户询问函数签名时在运行时进行计算。

计算表达式时有哪些命名空间可用?它在内置函数所在的模块上下文中计算。因此,如果您的模块有一个名为 max_widgets 的属性,您可以简单地使用它

foo: Py_ssize_t = max_widgets

如果在当前模块中找不到该符号,它将失败并转到 sys.modules 中查找。例如,它可以找到 sys.maxsize。(由于您无法预先知道用户会将哪些模块加载到其解释器中,因此最好将自己限制在 Python 本身预加载的模块中。)

仅在运行时计算默认值意味着 Argument Clinic 无法计算正确的等效 C 默认值。所以你需要明确地告诉它。当您使用表达式时,您还必须使用转换器的 c_default 参数指定 C 中的等效表达式

foo: Py_ssize_t(c_default="PY_SSIZE_T_MAX - 1") = sys.maxsize - 1

另一个复杂之处:Argument Clinic 无法预先知道您提供的表达式是否有效。它会对其进行解析以确保它看起来合法,但它无法实际知道。在使用表达式指定在运行时保证有效的变量时,您必须非常小心!

最后,由于表达式必须可以表示为静态 C 值,因此对合法表达式有很多限制。以下是不允许使用的 Python 特性列表

  • 函数调用。

  • 内联 if 语句 (3 if foo else 5)。

  • 自动序列解包 (*[1, 2, 3])。

  • 列表/集合/字典解析和生成器表达式。

  • 元组/列表/集合/字典字面量。

如何使用返回转换器

默认情况下,Argument Clinic 为您生成的 impl 函数返回 PyObject *。但您的 C 函数通常会计算一些 C 类型,然后在最后将其转换为 PyObject *。Argument Clinic 处理将您的输入从 Python 类型转换为本机 C 类型——为什么不使用它将您的返回值从本机 C 类型转换为 Python 类型呢?

这就是“返回转换器”的作用。它将您的 impl 函数更改为返回一些 C 类型,然后向生成的(非 impl)函数添加代码以处理将该值转换为适当的 PyObject *

返回转换器的语法类似于参数转换器的语法。您指定返回转换器就像它在函数本身上是一个返回注释,使用 -> 符号。

例如

/*[clinic input]
add -> int

    a: int
    b: int
    /

[clinic start generated code]*/

返回转换器的行为与参数转换器非常相似;它们接受参数,参数都是仅限关键字的,如果您不更改任何默认参数,则可以省略括号。

(如果你同时使用 "as" 一个返回转换器,那么 "as" 应该在返回转换器之前。)

在使用返回转换器时,有一个额外的复杂情况:如何指示发生了错误?通常,一个函数返回一个有效的(非 NULL)指针表示成功,NULL 表示失败。但是,如果你使用一个整数返回转换器,那么所有整数都是有效的。Argument Clinic 如何检测错误?它的解决方案:每个返回转换器隐式查找一个表示错误的特殊值。如果你返回该值,并且已经设置了一个错误(c:func:PyErr_Occurred 返回一个真值),那么生成的代码将传播该错误。否则,它将像往常一样对返回的值进行编码。

目前 Argument Clinic 只支持少数几个返回转换器

bool
double
float
int
long
Py_ssize_t
size_t
unsigned int
unsigned long

它们都不接受参数。对于所有这些,返回 -1 表示错误。

要查看 Argument Clinic 支持的所有返回转换器,以及它们的(如果有)参数,只需运行 Tools/clinic/clinic.py --converters 即可获得完整列表。

如何克隆现有函数

如果你有许多看起来相似的函数,你可能可以使用 Clinic 的“克隆”功能。当你克隆一个现有函数时,你可以重复使用

  • 它的参数,包括

    • 它们的名字,

    • 它们的转换器,带所有参数,

    • 它们的默认值,

    • 它们每个参数的文档字符串,

    • 它们的类型(它们是仅限位置、位置或关键字,还是仅限关键字),以及

  • 它的返回转换器。

从原始函数中唯一不会被复制的是它的文档字符串;语法允许你指定一个新的文档字符串。

以下是克隆一个函数的语法

/*[clinic input]
module.class.new_function [as c_basename] = module.class.existing_function

Docstring for new_function goes here.
[clinic start generated code]*/

(这些函数可以位于不同的模块或类中。我在示例中写了 module.class 只是为了说明你必须对两个函数使用完整路径。)

抱歉,没有语法可以部分克隆一个函数,或者克隆一个函数然后修改它。克隆是一个全有或全无的命题。

此外,你克隆的函数必须之前已在当前文件中定义。

如何调用 Python 代码

其余的高级主题要求你编写 Python 代码,它驻留在你的 C 文件中并修改 Argument Clinic 的运行时状态。这很简单:你只需定义一个 Python 块。

一个 Python 块使用与 Argument Clinic 函数块不同的分隔符行。它看起来像这样

/*[python input]
# python code goes here
[python start generated code]*/

Python 块中的所有代码在解析时执行。在块中写入 stdout 的所有文本都重定向到块后的“输出”中。

作为一个示例,这里有一个 Python 块,它向 C 代码添加了一个静态整数变量

/*[python input]
print('static int __ignored_unused_variable__ = 0;')
[python start generated code]*/
static int __ignored_unused_variable__ = 0;
/*[python checksum:...]*/

如何使用“self 转换器”

Argument Clinic 使用默认转换器自动为你添加一个“self”参数。它自动将此参数的 type 设置为你声明类型时指定的“指向实例的指针”。但是,你可以覆盖 Argument Clinic 的转换器并自己指定一个。只需在块中添加你自己的self 参数作为第一个参数,并确保它的转换器是 self_converter 或其子类的实例。

有什么用?这让你可以覆盖 self 的类型,或者给它一个不同的默认名称。

如何指定要将 self 转换为的自定义类型?如果你只有一个或两个具有相同 self 类型的函数,你可以直接使用 Argument Clinic 现有的 self 转换器,将你想要使用的类型作为 type 参数传递进去。

/*[clinic input]

_pickle.Pickler.dump

  self: self(type="PicklerObject *")
  obj: object
  /

Write a pickled representation of the given object to the open file.
[clinic start generated code]*/

另一方面,如果你有很多函数将使用相同的 self 类型,最好创建自己的转换器,对 self_converter 进行子类化,但覆盖 type 成员。

/*[python input]
class PicklerObject_converter(self_converter):
    type = "PicklerObject *"
[python start generated code]*/

/*[clinic input]

_pickle.Pickler.dump

  self: PicklerObject
  obj: object
  /

Write a pickled representation of the given object to the open file.
[clinic start generated code]*/

如何使用“定义类”转换器

Argument Clinic 便于访问方法的定义类。这对于需要获取模块级别状态的 堆类型 方法很有用。使用 PyType_FromModuleAndSpec() 将新的堆类型与模块关联。你现在可以在定义类上使用 PyType_GetModuleState() 来获取模块状态,例如从模块方法中获取。

来自 Modules/zlibmodule.c 的示例。首先,将 defining_class 添加到 clinic 输入中

/*[clinic input]
zlib.Compress.compress

  cls: defining_class
  data: Py_buffer
    Binary data to be compressed.
  /

在运行 Argument Clinic 工具后,将生成以下函数签名

/*[clinic start generated code]*/
static PyObject *
zlib_Compress_compress_impl(compobject *self, PyTypeObject *cls,
                            Py_buffer *data)
/*[clinic end generated code: output=6731b3f0ff357ca6 input=04d00f65ab01d260]*/

以下代码现在可以使用 PyType_GetModuleState(cls) 来获取模块状态

zlibstate *state = PyType_GetModuleState(cls);

每个方法只能有一个参数使用此转换器,它必须出现在 self 之后,或者,如果未使用 self,则作为第一个参数。参数的类型将为 PyTypeObject *。参数不会出现在 __text_signature__ 中。

defining_class 转换器与 __init__()__new__() 方法不兼容,这些方法不能使用 METH_METHOD 约定。

不能将 defining_class 与槽方法一起使用。为了从这种方法中获取模块状态,请使用 PyType_GetModuleByDef() 来查找模块,然后使用 PyModule_GetState() 来获取模块状态。来自 Modules/_threadmodule.csetattro 槽方法的示例

static int
local_setattro(localobject *self, PyObject *name, PyObject *v)
{
    PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &thread_module);
    thread_module_state *state = get_thread_state(module);
    ...
}

另请参阅 PEP 573

如何编写自定义转换器

转换器是一个从 CConverter 继承的 Python 类。自定义转换器的主要目的是针对使用 O& 格式单元解析的参数——解析此类参数意味着调用 PyArg_ParseTuple() “转换器函数”。

您的转换器类应命名为 ConverterName_converter。按照此约定,您的转换器类将自动在 Argument Clinic 中注册,其转换器名称将是您的转换器类的名称,去掉 _converter 后缀。

不要对 CConverter.__init__() 进行子类化,而要编写 converter_init() 方法。 converter_init() 始终接受一个self 参数。在self 之后,所有其他参数必须是仅关键字的。在 Argument Clinic 中传递给转换器的任何参数都将传递到您的 converter_init() 方法。有关您可能希望在子类中指定的成员的列表,请参阅 CConverter

以下是从 Modules/zlibmodule.c 中获取的最简单的自定义转换器示例:

/*[python input]

class ssize_t_converter(CConverter):
    type = 'Py_ssize_t'
    converter = 'ssize_t_converter'

[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=35521e4e733823c7]*/

此块向 Argument Clinic 添加了一个名为 ssize_t 的转换器。声明为 ssize_t 的参数将声明为类型 Py_ssize_t,并将由 'O&' 格式单元解析,该单元将调用 ssize_t_converter() 转换器 C 函数。 ssize_t 变量自动支持默认值。

更复杂的自定义转换器可以插入自定义 C 代码来处理初始化和清理。您可以在 CPython 源代码树中看到更多自定义转换器的示例;grep C 文件以查找字符串 CConverter

如何编写自定义返回转换器

编写自定义返回转换器与编写自定义转换器非常相似。除了它更简单一些,因为返回转换器本身就简单得多。

返回转换器必须是 CReturnConverter 的子类。目前还没有自定义返回转换器的示例,因为它们还没有被广泛使用。如果您希望编写自己的返回转换器,请阅读 Tools/clinic/clinic.py,特别是 CReturnConverter 及其所有子类的实现。

如何转换 METH_OMETH_NOARGS 函数

要使用 METH_O 转换函数,请确保该函数的单个参数使用 object 转换器,并将参数标记为仅限位置

/*[clinic input]
meth_o_sample

     argument: object
     /
[clinic start generated code]*/

要使用 METH_NOARGS 转换函数,请不要指定任何参数。

对于 METH_O,您仍然可以使用 self 转换器、返回转换器,并为 object 转换器指定一个 type 参数。

如何转换 tp_newtp_init 函数

您可以转换 tp_newtp_init 函数。只需根据需要将它们命名为 __new____init__。说明

  • __new__ 生成的函数名称不会以 __new__ 结尾,就像默认情况下那样。它只是类名,已转换为有效的 C 标识符。

  • 不会为这些函数生成 PyMethodDef #define

  • __init__ 函数返回 int,而不是 PyObject *

  • 使用文档字符串作为类文档字符串。

  • 尽管 __new____init__ 函数必须始终同时接受 argskwargs 对象,但在转换时,您可以为这些函数指定您喜欢的任何签名。(如果您的函数不支持关键字,则生成的解析函数在接收到任何关键字时会引发异常。)

如何更改和重定向 Clinic 的输出

将 Clinic 的输出与您常规手写 C 代码穿插在一起可能会带来不便。幸运的是,Clinic 是可配置的:您可以缓冲其输出以供稍后(或更早)打印,或将其输出写入单独的文件。您还可以为 Clinic 生成的输出的每一行添加前缀或后缀。

虽然以这种方式更改 Clinic 的输出可以提高可读性,但它可能会导致 Clinic 代码在定义之前使用类型,或者您的代码尝试在定义之前使用 Clinic 生成的代码。可以通过重新排列文件中的声明或移动 Clinic 生成的代码的位置轻松解决这些问题。(这就是 Clinic 的默认行为将所有内容输出到当前块的原因;虽然许多人认为这会妨碍可读性,但它永远不需要重新排列您的代码来修复定义前使用的问题。)

让我们从定义一些术语开始

字段

在此上下文中,字段是 Clinic 输出的一个子部分。例如,PyMethodDef 结构的 #define 是一个字段,称为 methoddef_define。Clinic 可以在每个函数定义中输出七个不同的字段

docstring_prototype
docstring_definition
methoddef_define
impl_prototype
parser_prototype
parser_definition
impl_definition

所有名称都采用 "<a>_<b>" 的形式,其中 "<a>" 是表示的语义对象(解析函数、impl 函数、文档字符串或 methoddef 结构),"<b>" 表示字段的语句类型。以 "_prototype" 结尾的字段名称表示该对象的向前声明,不包含对象的实际主体/数据;以 "_definition" 结尾的字段名称表示对象的实际定义,包含对象的实际主体/数据。("methoddef" 是特殊的,它是唯一以 "_define" 结尾的字段,表示它是一个预处理器 #define。)

目标

目标是 Clinic 可以向其写入输出的位置。有五个内置目标

默认目标:打印在当前 Clinic 块的输出部分。

缓冲区

一个文本缓冲区,可以在其中保存文本以供以后使用。发送到此处的文本将附加到任何现有文本的末尾。在 Clinic 完成处理文件时,缓冲区中留有文本是一个错误。

文件

一个单独的“clinic 文件”,它将由 Clinic 自动创建。为文件选择的名称是 {basename}.clinic{extension},其中 basenameextension 被分配为在当前文件上运行 os.path.splitext() 的输出。(示例:_pickle.cfile 目标将写入 _pickle.clinic.c。)

重要提示:使用 file 目标时, 必须检入 生成的文件!

两遍

一个类似于 buffer 的缓冲区。但是,两遍缓冲区只能转储一次,并且会打印在所有处理过程中发送到它的所有文本,即使来自转储点之后的 Clinic 块。

抑制

文本被抑制——丢弃。

Clinic 定义了五个新指令,可用于重新配置其输出。

第一个新指令是 dump

dump <destination>

这会将指定目标的当前内容转储到当前块的输出中,并清空它。这仅适用于 buffertwo-pass 目标。

第二个新指令是 output。最基本的 output 形式如下

output <field> <destination>

这告诉 Clinic 将 field 输出到 destinationoutput 还支持一个特殊的元目标,称为 everything,它告诉 Clinic 将 所有 字段输出到该 destination

output 还有许多其他功能

output push
output pop
output preset <preset>

output pushoutput pop 允许您在内部配置堆栈上推送和弹出配置,以便您可以暂时修改输出配置,然后轻松还原以前的配置。只需在更改前推送以保存当前配置,然后在希望还原以前的配置时弹出即可。

output preset 将 Clinic 的输出设置为以下几个内置预设配置之一

Clinic 的原始起始配置。在输入块之后立即写入所有内容。

抑制 parser_prototypedocstring_prototype,将所有其他内容写入 block

文件

旨在将所有内容写入它可以写入的“clinic 文件”。然后您在文件顶部附近 #include 此文件。您可能需要重新排列文件以使其正常工作,尽管通常这仅意味着为各种 typedefPyTypeObject 定义创建前向声明。

抑制 parser_prototypedocstring_prototype,将 impl_definition 写入 block,并将所有其他内容写入 file

默认文件名是 "{dirname}/clinic/{basename}.h"

缓冲区

保存 Clinic 的大部分输出,以便在文件结尾附近写入您的文件。对于实现模块或内置类型的 Python 文件,建议您将缓冲区转储到模块或内置类型的静态结构上方;这些通常非常接近结尾。如果您的文件在文件中间定义了静态 PyMethodDef 数组,则使用 buffer 可能需要比 file 更多的编辑。

抑制 parser_prototypeimpl_prototypedocstring_prototype,将 impl_definition 写入 block,并将所有其他内容写入 file

两遍

类似于 buffer 预设,但将前向声明写入 two-pass 缓冲区,并将定义写入 buffer。这类似于 buffer 预设,但可能比 buffer 所需的编辑更少。将 two-pass 缓冲区转储到文件顶部附近,并将 buffer 转储到文件末尾附近,就像使用 buffer 预设时所做的那样。

抑制 impl_prototype,将 impl_definition 写入 block,将 docstring_prototypemethoddef_defineparser_prototype 写入 two-pass,将其他所有内容写入 buffer

partial-buffer

类似于 buffer 预设,但将更多内容写入 block,仅将真正大块的生成代码写入 buffer。这完全避免了 buffer 的定义在使用之前的问题,代价很小,即块的输出中内容略多。将 buffer 转储到末尾附近,就像使用 buffer 预设时所做的那样。

抑制 impl_prototype,将 docstring_definitionparser_definition 写入 buffer,将其他所有内容写入 block

第三个新指令是 destination

destination <name> <command> [...]

这将对名为 name 的目标执行操作。

有两个已定义的子命令:newclear

new 子命令的工作方式如下

destination <name> new <type>

这将创建一个名为 <name> 且类型为 <type> 的新目标。

有五种目标类型

抑制

丢弃文本。

将文本写入当前块。这是 Clinic 最初所做的。

缓冲区

一个简单的文本缓冲区,如上面内置目标“buffer”。

文件

一个文本文件。文件目标需要一个额外的参数,即用于构建文件名的一个模板,如下所示

destination <name> new <type> <file_template>

模板可以在内部使用三个字符串,这些字符串将被文件名中的位替换

{path}

文件的完整路径,包括目录和完整文件名。

{dirname}

文件所在目录的名称。

{basename}

仅文件名,不包括目录。

{basename_root}

剪切掉扩展名的基本名称(包括最后一个“.”之前的所有内容,但不包括“.”)。

{basename_extension}

最后一个“.”及其之后的所有内容。如果基本名称不包含句点,则为空字符串。

如果文件名中没有句点,则 {basename}{filename} 相同,并且 {extension} 为空。 {basename}{extension} 始终与 {filename} 完全相同。

两遍

双通道缓冲区,如上文中的“双通道”内置目标。

clear 子命令的工作方式如下

destination <name> clear

它将移除目标中到此为止累积的所有文本。(我不知道你为什么要这样做,但我认为在某人进行实验时这可能很有用。)

第四个新指令是 set

set line_prefix "string"
set line_suffix "string"

set 允许你在 Clinic 中设置两个内部变量。 line_prefix 是一个字符串,它将被添加到 Clinic 输出的每一行之前; line_suffix 是一个字符串,它将被添加到 Clinic 输出的每一行之后。

这两个都支持两种格式字符串

{block comment start}

转换为字符串 /*,即 C 文件的开始注释文本序列。

{block comment end}

转换为字符串 */,即 C 文件的结束注释文本序列。

最后一个新指令是你不应该直接使用的,称为 preserve

preserve

这告诉 Clinic 应保留输出的当前内容,不进行修改。当将输出转储到 file 文件中时,Clinic 会在内部使用它;将其包装在 Clinic 块中可以让 Clinic 使用其现有的校验和功能来确保文件在被覆盖之前未被手动修改。

如何使用 #ifdef 技巧

如果你正在转换一个并非在所有平台上都可用的函数,有一个技巧可以让你轻松一些。现有代码可能如下所示

#ifdef HAVE_FUNCTIONNAME
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */

然后在底部的 PyMethodDef 结构中,现有代码将具有

#ifdef HAVE_FUNCTIONNAME
{'functionname', ... },
#endif /* HAVE_FUNCTIONNAME */

在这种情况下,你应该将 impl 函数的主体用 #ifdef 括起来,如下所示

#ifdef HAVE_FUNCTIONNAME
/*[clinic input]
module.functionname
...
[clinic start generated code]*/
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */

然后,从 PyMethodDef 结构中移除那三行,用 Argument Clinic 生成的宏替换它们

MODULE_FUNCTIONNAME_METHODDEF

(你可以在生成的代码中找到此宏的真实名称。或者你可以自己计算:它是你的函数在块的第一行中定义的名称,但将句点更改为下划线,大写,并在末尾添加 "_METHODDEF"。)

你可能想知道:如果未定义 HAVE_FUNCTIONNAME 怎么办? MODULE_FUNCTIONNAME_METHODDEF 宏也不会被定义!

Argument Clinic 的巧妙之处就在这里。它实际上可以检测 Argument Clinic 块是否可能被 #ifdef 停用。当这种情况发生时,它会生成一些类似这样的额外代码

#ifndef MODULE_FUNCTIONNAME_METHODDEF
    #define MODULE_FUNCTIONNAME_METHODDEF
#endif /* !defined(MODULE_FUNCTIONNAME_METHODDEF) */

这意味着宏始终有效。如果定义了函数,这将变成正确的结构,包括尾随逗号。如果函数未定义,这将变成什么都没有。

但是,这会导致一个棘手的问题:在使用“块”输出预设时,Argument Clinic 应该将此额外代码放在哪里?它不能放在输出块中,因为这可能会被 #ifdef 停用。(这就是关键!)

在这种情况下,Argument Clinic 会将额外代码写入“缓冲区”目标。这可能意味着您会收到 Argument Clinic 的投诉

Warning in file "Modules/posixmodule.c" on line 12357:
Destination buffer 'buffer' not empty at end of file, emptying.

当这种情况发生时,只需打开您的文件,找到 Argument Clinic 添加到您的文件中的 dump buffer 块(它将位于最底部),然后将其移动到使用该宏的 PyMethodDef 结构上方。

如何在 Python 文件中使用 Argument Clinic

实际上,可以使用 Argument Clinic 预处理 Python 文件。当然,使用 Argument Clinic 块没有意义,因为输出对 Python 解释器没有任何意义。但是,使用 Argument Clinic 运行 Python 块可以让您将 Python 用作 Python 预处理器!

由于 Python 注释不同于 C 注释,因此嵌入在 Python 文件中的 Argument Clinic 块看起来略有不同。它们看起来像这样

#/*[python input]
#print("def foo(): pass")
#[python start generated code]*/
def foo(): pass
#/*[python checksum:...]*/

如何使用受限 C API

如果 Argument Clinic 输入 位于包含 #define Py_LIMITED_API 的 C 源文件中,Argument Clinic 将生成使用 受限 API 来解析参数的 C 代码。这样做的好处是,生成的代码不会使用私有函数。但是,在某些情况下,这可能导致 Argument Clinic 生成的代码效率较低。性能损失的程度将取决于参数(类型、数量等)。

在 3.13 版本中添加。

如何覆盖生成的签名

您可以使用 @text_signature 指令来覆盖文档字符串中默认生成的签名。这对于 Argument Clinic 无法处理的复杂签名非常有用。@text_signature 指令接受一个参数:作为字符串的自定义签名。提供的签名将逐字复制到生成的文档字符串中。

来自 Objects/codeobject.c 的示例

/*[clinic input]
@text_signature "($self, /, **changes)"
code.replace
    *
    co_argcount: int(c_default="self->co_argcount") = unchanged
    co_posonlyargcount: int(c_default="self->co_posonlyargcount") = unchanged
    # etc ...

    Return a copy of the code object with new values for the specified fields.
[clinic start generated output]*/

生成的文档字符串最终看起来像这样

replace($self, /, **changes)
--

Return a copy of the code object with new values for the specified fields.

如何在 Argument Clinic 中使用临界区

您可以使用 @critical_section 指令指示 Argument Clinic 将对“impl”函数的调用包装在“Python 临界区”中。在没有全局解释器锁(“GIL”)的 CPython 构建中,需要临界区才能在不导致线程之间死锁的情况下实现线程安全性。进入临界区时,将获取与装饰函数的第一个参数关联的每个对象锁。退出临界区时释放锁。

Python 临界区在使用 GIL 构建 CPython 时为无操作。有关临界区的更多详细信息,请参阅 Include/internal/pycore_critical_section.hPEP 703

来自 Modules/_io/bufferedio.c 的示例

/*[clinic input]
@critical_section
_io._Buffered.close
[clinic start generated code]*/

生成的胶合代码如下所示

static PyObject *
_io__Buffered_close(buffered *self, PyObject *Py_UNUSED(ignored))
{
   PyObject *return_value = NULL;

   Py_BEGIN_CRITICAL_SECTION(self);
   return_value = _io__Buffered_close_impl(self);
   Py_END_CRITICAL_SECTION();

   return return_value;
}

您可以通过将 C 变量名作为参数提供给 @critical_section 指令,来锁定一个或两个附加对象。此示例来自 Modules/_weakref.c,它采用一个附加参数(一个名为 object 的 C 变量)

/*[clinic input]
@critical_section object
_weakref.getweakrefcount -> Py_ssize_t

object: object
/
Return the number of weak references to 'object'.
[clinic start generated code]*/

生成的胶合代码如下所示

static PyObject *
_weakref_getweakrefs(PyObject *module, PyObject *object)
{
  PyObject *return_value = NULL;

  Py_BEGIN_CRITICAL_SECTION(object);
  return_value = _weakref_getweakrefs_impl(module, object);
  Py_END_CRITICAL_SECTION();

  return return_value;
}

在 3.13 版本中添加。

如何声明 PyGetSetDef(“getter/setter”)函数

“Getter” 和 “setter” 是在 PyGetSetDef 结构中定义的 C 函数,用于方便类进行类似于 property 的访问。您可以使用 @getter@setter 指令来使用 Argument Clinic 生成“impl”函数。

此示例取自 Modules/_io/textio.c,展示了 @getter@setter@critical_section 指令(在不造成线程之间死锁的情况下实现线程安全)结合使用

/*[clinic input]
@critical_section
@getter
_io.TextIOWrapper._CHUNK_SIZE
[clinic start generated code]*/

/*[clinic input]
@critical_section
@setter
_io.TextIOWrapper._CHUNK_SIZE
[clinic start generated code]*/

生成的胶合代码如下所示

static PyObject *
_io_TextIOWrapper__CHUNK_SIZE_get(textio *self, void *Py_UNUSED(context))
{
    PyObject *return_value = NULL;

    Py_BEGIN_CRITICAL_SECTION(self);
    return_value = _io_TextIOWrapper__CHUNK_SIZE_get_impl(self);
    Py_END_CRITICAL_SECTION();

    return return_value;
}

static int
_io_TextIOWrapper__CHUNK_SIZE_set(textio *self, PyObject *value, void *Py_UNUSED(context))
{
    int return_value;
    Py_BEGIN_CRITICAL_SECTION(self);
    return_value = _io_TextIOWrapper__CHUNK_SIZE_set_impl(self, value);
    Py_END_CRITICAL_SECTION();
    return return_value;
}

注意

Getter 和 setter 必须声明为独立的函数。Argument Clinic 会隐式添加“setter”的value 参数。可以通过将文档字符串添加到 @getter 来为属性创建文档字符串。

然后,实现将与由 property 装饰的 Python 方法相同

>>> import sys, _io
>>> a = _io.TextIOWrapper(sys.stdout)
>>> a._CHUNK_SIZE
8192
>>> a._CHUNK_SIZE = 30
>>> a._CHUNK_SIZE
30

在 3.13 版本中添加。

如何弃用按位置或按关键字传递参数

Argument Clinic 提供的语法可以生成弃用按位置或按关键字传递 参数 的位置或关键字 参数 的代码。例如,假设我们有一个模块级函数 foo.myfunc(),它有五个参数:一个仅限位置的参数 a,三个位置或关键字参数 bcd,以及一个仅限关键字的参数 e

/*[clinic input]
module foo
myfunc
    a: int
    /
    b: int
    c: int
    d: int
    *
    e: int
[clinic start generated output]*/

我们现在想让b 参数仅限位置,而d 参数仅限关键字;但是,我们必须等待两个版本才能进行这些更改,因为 Python 的向后兼容性策略要求如此(参见PEP 387)。对于此示例,假设我们正处于 Python 3.12 的开发阶段:这意味着当b 参数的某个参数通过关键字传递或d 参数的某个参数通过位置传递时,我们将在 Python 3.12 中引入弃用警告,并且最早在 Python 3.14 中,我们才能分别使它们仅限位置和仅限关键字。

我们可以使用 Argument Clinic 通过 [from ...] 语法发出所需的弃用警告,方法是在b 参数正下方添加行 / [from 3.14],并在d 参数正上方添加行 * [from 3.14]

/*[clinic input]
module foo
myfunc
    a: int
    /
    b: int
    / [from 3.14]
    c: int
    * [from 3.14]
    d: int
    *
    e: int
[clinic start generated output]*/

接下来,重新生成 Argument Clinic 代码(make clinic),并为新行为添加单元测试。

参数 d参数 通过位置传递(例如 myfunc(1, 2, 3, 4, e=5))或参数 b 的参数通过关键字传递(例如 myfunc(1, b=2, c=3, d=4, e=5))时,生成的代码现在会发出 DeprecationWarning。如果在弃用期结束后(即指定 Python 版本的 alpha 阶段开始时)尚未从 Argument Clinic 输入中删除 [from ...] 行,还会生成 C 预处理器指令以发出编译器警告。

让我们回到我们的示例并跳过两年:Python 3.14 开发现已进入 alpha 阶段,但我们忘记更新 myfunc() 的 Argument Clinic 代码!对我们来说幸运的是,现在生成了编译器警告

In file included from Modules/foomodule.c:139:
Modules/clinic/foomodule.c.h:139:8: warning: In 'foomodule.c', update the clinic input of 'mymod.myfunc'. [-W#warnings]
 #    warning "In 'foomodule.c', update the clinic input of 'mymod.myfunc'. [-W#warnings]"
      ^

我们现在通过使a 仅限位置且c 仅限关键字来关闭弃用阶段;用a 下方的 / [from ...] 行替换b 下方的 / [from ...] 行,用e 上方的 * [from ...] 行替换d 上方的 * [from ...]

/*[clinic input]
module foo
myfunc
    a: int
    b: int
    /
    c: int
    *
    d: int
    e: int
[clinic start generated output]*/

最后,运行 make clinic 以重新生成 Argument Clinic 代码,并更新单元测试以反映新行为。

注意

如果您忘记在 alpha 和 beta 阶段更新输入块,那么当候选版本阶段开始时,编译器警告将变为编译器错误。