Argument Clinic指南

作者

拉里黑斯廷斯

摘要

Argument Clinic是cpython c文件的预处理器。它的目的是自动化为“builtins”编写参数解析代码所涉及的所有样板文件。本文介绍如何将第一个C函数转换为与Argument Clinic一起使用,然后介绍一些关于Argument Clinic用法的高级主题。

目前,Argument Clinic仅被认为是针对CPython的内部诊所。它不支持用于cpython之外的文件,也不保证将来版本的向后兼容性。换句话说:如果您为cpython维护一个外部C扩展,那么欢迎您在自己的代码中使用Argument Clinic进行实验。但是下一个版本的cpython附带的Argument Clinic版本 能够 完全不兼容并破坏所有代码。

Argument Clinic的目标

Argument Clinic的主要目标是接管cpython内部所有的参数解析代码。这意味着,当您将一个函数转换为与Argument Clinic一起使用时,该函数不应该再进行任何自己的参数分析,Argument Clinic生成的代码对您来说应该是一个“黑盒”,其中cpython在顶部调用,而您的代码在底部被调用,使用 PyObject *args (也许) PyObject *kwargs )神奇地转换成您需要的C变量和类型。

为了使Argument Clinic达到它的首要目标,它必须易于使用。目前,使用cpython的参数解析库是一项繁琐的工作,需要在大量地方维护冗余信息。当你使用Argument Clinic时,你不必重复你自己。

显然,没有人会想使用Argument Clinic,除非它能解决他们的问题,并且不会产生新的问题。所以,Argument Clinic生成正确的代码是至关重要的。如果代码也更快,那就更好了,但至少它不应该引入主要的速度回归。(最终Argument Clinic 应该 为了加快速度,我们可以重写它的代码生成器来生成定制的参数解析代码,而不是调用通用的cpython参数解析库。这将使最快的参数解析成为可能!)

此外,Argument Clinic必须足够灵活,可以使用任何方法进行参数解析。python有一些函数和一些非常奇怪的解析行为;argument clinic的目标是支持所有这些函数。

最后,Argument Clinic最初的动机是为CPython内置设备提供反省“签名”。过去,如果传入一个内置函数,自省查询函数会抛出一个异常。在Argument Clinic,这是过去的事了!

当你在Argument Clinic工作时,你应该记住一个想法:你提供的信息越多,它就能做得更好。毫无疑问,Argument Clinic现在相对简单。但是随着它的发展,它会变得更加复杂,它应该能够用你提供的所有信息做许多有趣和聪明的事情。

基本概念和用法

Argument Clinic和凯普顿合作,你可以在 Tools/clinic/clinic.py . 如果运行该脚本,将C文件指定为参数:

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

Argument Clinic将扫描文件,查找与此完全相同的行:

/*[clinic input]

当它找到一个时,它会读取到一行中的所有内容,看起来完全像这样:

[clinic start generated code]*/

这两行之间的所有内容都是Argument Clinic的输入。所有这些行,包括开始和结束注释行,统称为Argument Clinic“块”。

当Argument Clinic解析其中一个块时,它生成输出。该输出在块后立即重写到C文件中,后面跟着一个包含校验和的注释。现在,Argument Clinic块如下所示:

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

如果您再次在同一个文件上运行Argument Clinic,Argument Clinic将丢弃旧的输出,并用新的校验和行写出新的输出。但是,如果输入没有改变,输出也不会改变。

您不应该修改Argument Clinic块的输出部分。相反,改变输入直到它产生你想要的输出。(这是校验和的目的,用于检测是否有人更改了输出,因为下次Argument Clinic写出新输出时,这些编辑将丢失。)

为了清楚起见,以下是我们将与Argument Clinic一起使用的术语:

  • 评论的第一行 (/*[clinic input]起始行 .

  • 初始注释的最后一行 ([clinic start generated code]*/端线 .

  • 最后一行 (/*[clinic end generated code: checksum=...]*/校验和线 .

  • 在开始线和结束线之间是 input .

  • 在结束行和校验和行之间是 output .

  • 从起始行到校验和行(包括在内)的所有文本都是 . (Argument Clinic尚未成功处理的块没有输出或校验和行,但仍被视为块。)

转换第一个函数

了解“Argument Clinic”如何工作的最好方法是将函数转换为使用函数。下面是将函数转换为与Argument Clinic一起工作所需遵循的最基本步骤。请注意,对于计划签入到cpython的代码,您确实应该使用文档中稍后将看到的一些高级概念(如“返回转换器”和“自转换器”),进一步进行转换。但我们将保持简单的演练,以便您可以学习。

我们潜水吧!

  1. 确保您正在使用新更新的cpython主干结帐。

  2. 找到一个调用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() ,并且还没有转换为与Argument Clinic一起工作。例如,我正在使用 _pickle.Pickler.dump() .

  3. 如果调用给 PyArg_Parse 函数使用以下任何格式单位:

    O&
    O!
    es
    es#
    et
    et#
    

    或者如果它有多个调用 PyArg_ParseTuple() ,您应该选择一个不同的函数。Argument Clinic does 支持所有这些场景。但是这些是高级主题,让我们为您的第一个函数做一些简单的事情。

    此外,如果函数对 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 如果它对同一个参数支持不同的类型,或者如果该函数使用Pyarg_Parse函数之外的某个函数来解析其参数,那么它可能不适合转换为Argument Clinic。Argument Clinic不支持泛型函数或多态参数。

  4. 在函数上方添加以下样板文件,创建块:

    /*[clinic input]
    [clinic start generated code]*/
    
  5. 剪切docstring并将其粘贴到 [clinic] 行,删除所有使其成为正确引用的C字符串的垃圾。完成后,应该只保留基于左边距的文本,行宽不超过80个字符。(Argument Clinic将在docstring中保留缩进。)

    如果旧docstring的第一行看起来像函数签名,则将该行丢弃。(使用时docstring不再需要它 help() 在将来的内置函数上,第一行将根据函数的签名自动生成。)

    样品:

    /*[clinic input]
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  6. 如果你的docstring没有“摘要”行,Argument Clinic会抱怨。所以让我们确定它有一个。“摘要”行应该是一个段落,在docstring的开头由一个80列的行组成。

    (我们的示例docstring只包含一个摘要行,因此不必为此步骤更改示例代码。)

  7. 在docstring上方,输入函数的名称,后跟一行空白。这应该是函数的python名称,并且应该是函数的完整虚线路径,它应该以模块的名称开始,包括任何子模块,如果函数是一个类上的方法,它也应该包括类名。

    样品:

    /*[clinic input]
    _pickle.Pickler.dump
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  8. 如果这是该模块或类第一次在此C文件中与参数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]*/
    
  9. 向函数声明每个参数。每个参数都应该有自己的行。所有参数行都应该从函数名和docstring缩进。

    这些参数行的一般形式如下:

    name_of_parameter: converter
    

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

    name_of_parameter: converter = default_value
    

    Argument Clinic对“默认值”的支持非常复杂;请参阅 the section below on default values 更多信息。

    在参数下方添加空行。

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

    对于每个参数,从 PyArg_Parse() 设置参数格式并指定 that 作为它的转换器,作为带引号的字符串。(“格式单位”是 format 参数,告诉参数解析函数变量的类型以及如何转换它。有关格式单位的更多信息,请参见 分析参数并生成值

    对于多字符格式的单位,如 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]*/
    
  10. 如果你的功能 | 在格式字符串中,意味着某些参数具有默认值,您可以忽略它。Argument Clinic根据参数是否具有默认值来推断哪些参数是可选的。

    如果你的功能 $ 在格式字符串中,这意味着它只接受关键字参数,请指定 * 在第一个只包含关键字的参数前面的行上,缩进与参数行相同。

    (_pickle.Pickler.dump 两者都没有,所以我们的样本不变。)

  11. 如果现有的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]*/
    
  12. 为每个参数编写一个每个参数的docstring是很有帮助的。但是每个参数的docstrings是可选的;如果愿意,您可以跳过这个步骤。

    下面是如何添加每个参数的docstring。每个参数docstring的第一行必须缩进到参数定义之外。第一行的左边距为每个参数docstring建立了整个左边距;您所写的所有文本都将超过这个值。如果你愿意,你可以写任意多行文字。

    样品:

    /*[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]*/
    
  13. 保存并关闭文件,然后运行 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 文件,通常在诊所模块块之后:

    #include "clinic/_pickle.c.h"
    
  14. 仔细检查clinic生成的代码参数解析参数是否与现有代码基本相同。

    首先,确保两个地方使用相同的参数解析函数。现有代码必须调用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() ;确保参数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 此内置项的结构。

    如果这些项目中的任何一项在 任何方式 ,调整参数clinic函数规范并重新运行 Tools/clinic/clinic.py 直到他们 are 相同的。

  15. 注意,它输出的最后一行是“impl”函数的声明。这就是builtin的实现所在。删除正在修改的函数的现有原型,但保留左大括号。现在删除它的参数解析代码和它将参数转储到的所有变量的声明。注意,python参数现在是这个impl函数的参数;如果实现对这些变量使用了不同的名称,请修复它。

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

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

    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;
    
        ...
    
  16. 记住宏 PyMethodDef 此函数的结构?查找现有 PyMethodDef 此函数的结构,并将其替换为对宏的引用。(如果builtin在模块范围内,这可能非常接近文件的结尾;如果builtin是类方法,这可能在下面,但相对接近实现。)

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

    样品:

    static struct PyMethodDef Pickler_methods[] = {
        __PICKLE_PICKLER_DUMP_METHODDEF
        __PICKLE_PICKLER_CLEAR_MEMO_METHODDEF
        {NULL, NULL}                /* sentinel */
    };
    
  17. 编译,然后运行回归测试套件的相关部分。此更改不应引入任何新的编译时警告或错误,并且不应对Python的行为进行任何外部可见的更改。

    好吧,除了一个区别: inspect.signature() 运行函数现在应该提供一个有效的签名!

    恭喜您,您已将第一个功能移植到Argument Clinic!

高级主题

现在你已经有了一些与Argument Clinic合作的经验,是时候讨论一些高级话题了。

符号默认值

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

  • 数字常量(整数和浮点)

  • 字符串常量

  • True, False, and None

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

如果您好奇,可以在 from_builtin() 在里面 Lib/inspect.py .

(在未来,这可能需要更详细地说明,以允许像 CONSTANT - 1

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

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

例如,如果我们想重命名为 pickle.Pickler.dump ,看起来像这样:

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

...

现在将命名基函数 pickler_dumper() ,现在将命名impl函数 pickler_dumper_impl() .

类似地,如果您想给一个参数指定一个特定的python名称,可能会遇到问题,但是在c中,这个名称可能不方便。argument clinic允许您在python和c中使用相同的名称给一个参数指定不同的名称。 "as" 语法:

/*[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() ,它在 left 它所需参数的一方!另一个例子是 curses.window.addch() ,它有一组必须始终一起指定的两个参数。(参数被调用 xy ;如果调用传入的函数 x ,你也必须通过 y -如果你不进去 x 你不能进去 y 要么。

在任何情况下,Argument Clinic的目标都是支持对所有现有的cpython内置组件进行参数解析,而不改变它们的语义。因此,Argument Clinic支持使用这种称为 任选组 . 可选组是必须一起传递的参数组。它们可以在所需参数的左侧或右侧。他们可以 only 只能与位置参数一起使用。

注解

可选组包括 only 用于将多个调用的函数转换为 PyArg_ParseTuple() !使用的函数 any 解析参数的其他方法应该 几乎从不 使用可选组转换为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}} 在哪里 {{direction}} 要么是 rightleft 取决于组是在所需参数之前还是之后,以及 {{number}} 是一个单调递增的数字(从1开始),表示组与所需参数的距离。调用IMPL时,如果未使用此组,则此参数将设置为零;如果使用了此组,则此参数将设置为非零。(对于已用或未用,我的意思是参数是否在这个调用中接收到参数。)

  • 如果没有必需的参数,可选组的行为就好像它们在必需参数的右边一样。

  • 在歧义的情况下,参数解析代码倾向于左边的参数(在所需参数之前)。

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

  • 可选组包括 only 用于旧代码。请不要为新代码使用可选组。

使用实参诊所转换器,而不是“传统转换器”

为了节省时间,并尽量减少实现第一个参数转换诊所所需的学习量,上面的演练告诉您使用“遗留转换器”。传统转换器“是一种方便的工具,其设计明确地使将现有代码移植到Argument Clinic变得更容易。而且要清楚的是,在为python 3.4移植代码时,它们的使用是可以接受的。

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

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

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

  • 将来,我们可能会有一个新的参数解析库,它不局限于 PyArg_ParseTuple() 支持;这种灵活性不适用于使用传统转换器的参数。

因此,如果您不介意做一些额外的工作,请使用普通转换器而不是传统转换器。

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

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

c_default

此参数在C中定义时的默认值。具体来说,这将是“parse函数”中声明的变量的初始值设定项。参见 the section on default values 如何使用这个。指定为字符串。

annotation

此参数的批注值。目前不支持,因为 PEP 8 要求python库不能使用注释。

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

accept

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

接受 None ,添加 NoneType 这一套。

bitwise

仅支持无符号整数。此python参数的本机整数值将写入该参数,而不进行任何范围检查,即使是负值。

converter

仅支持 object 转换器。指定的名称 C "converter function" 用于将此对象转换为本机类型。

encoding

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

subclass_of

仅支持 object 转换器。要求python值是python类型的子类,如C所示。

type

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

zeroes

仅支持字符串。如果为真,则嵌入nul字节 ('\\0' )允许在值内。字符串的长度将作为名为 <parameter_name>_length .

请注意,并非所有可能的参数组合都有效。通常这些参数是由特定的 PyArg_ParseTuple 格式化单元 具有特定行为。例如,当前不能调用 unsigned_short 无需说明 bitwise=True . 虽然认为这样做是完全合理的,但是这些语义并没有映射到任何现有的格式单元。所以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'

object

'O!'

object(subclass_of='&PySomething_Type')

'O&'

object(converter='name_of_c_function')

'p'

bool

'S'

PyBytesObject

's'

str

's#'

str(zeroes=True)

's*'

Py_buffer(accept={buffer, str})

'U'

unicode

'u'

Py_UNICODE

'u#'

Py_UNICODE(zeroes=True)

'w*'

Py_buffer(accept={rwbuffer})

'Y'

PyByteArrayObject

'y'

str(accept={bytes})

'y#'

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

'y*'

Py_buffer

'Z'

Py_UNICODE(accept={str, NoneType})

'Z#'

Py_UNICODE(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*' 传统转换器),您 must 不调用 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 手工调用,理论上可以在运行时决定要传递到哪个编码字符串 PyArg_ParseTuple() . 但现在这个字符串必须在Argument Clinic预处理时进行硬编码。这种限制是经过深思熟虑的;它使支持这种格式单元变得更加容易,并且可能允许将来的优化。这种限制似乎并不不合理;cpython本身总是为格式单位以开头的参数传递静态硬编码编码编码字符串。 e .

参数默认值

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

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 .

指定为默认值的表达式

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

请考虑以下示例:

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本身预加载的模块中。)

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

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]

  • 列出/设置/听写理解和生成器表达式。

  • Tuple/list/set/dict literals.

使用回流转换器

默认情况下,诊所为您生成的impl函数参数返回 PyObject * . 但是C函数经常计算一些C类型,然后将其转换为 PyObject * 在最后一刻。Argument Clinic处理将您的输入从python类型转换为本机C类型的过程,为什么不把您的返回值从本机C类型转换为python类型呢?

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

返回转换器的语法与参数转换器的语法相似。您可以像在函数本身上指定返回转换器一样指定返回转换器。返回转换器的行为与参数转换器基本相同;它们接受参数,参数都是关键字,如果不更改任何默认参数,则可以省略括号。

(如果您同时使用 "as" and 您的函数的返回转换器, "as" 应该在回流转换器之前。)

在使用返回转换器时还有一个额外的复杂问题:如何指示发生了错误?通常,函数会返回一个有效的(非``空``)指针以获得成功,并且 NULL 因为失败。但如果使用整数返回转换器,则所有整数都是有效的。Argument Clinic如何检测错误?其解决方案是:每个返回转换器隐式地查找指示错误的特殊值。如果返回该值,并且设置了错误 (PyErr_Occurred() 返回一个真值),然后生成的代码将传播错误。否则,它会像正常一样对返回的值进行编码。

目前,Argument Clinic只支持少数返回转换器:

bool
int
unsigned int
long
unsigned int
size_t
Py_ssize_t
float
double
DecodeFSDefault

这些都不需要参数。对于前三个,返回-1以指示错误。为了 DecodeFSDefault ,返回类型为 const char * ;返回a NULL 指示错误的指针。

(还有一个实验 NoneType 转换器,让您返回 Py_None 关于成功还是 NULL 失败时,不必增加引用计数 Py_None . 我不确定它是否增加了足够的清晰度,值得使用。)

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

复制现有函数

如果您有许多类似的功能,您可能可以使用诊所的“复制”功能。复制现有函数时,可以重用:

  • 其参数,包括

    • 他们的名字,

    • 他们的转换器,所有参数,

    • 它们的默认值,

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

    • 他们的 kind (无论它们是仅位置的、位置的或关键字的,还是仅关键字的),以及

  • 它的返回转换器。

唯一没有从原始函数复制的是它的docstring;语法允许您指定一个新的docstring。

以下是复制函数的语法:

/*[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 在示例中,为了说明必须使用 both 函数。

抱歉,没有语法可以部分复制一个函数,或者复制一个函数然后修改它。复制是一个全无的主张。

此外,要从中复制的函数必须是在当前文件中先前定义的。

调用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:...]*/

使用“自转换器”

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]*/

使用“定义类”转换器

参数诊所便于访问方法的定义类。这对以下方面很有用 heap type 需要获取模块级状态的方法。使用 PyType_FromModuleAndSpec() 若要将新的堆类型与模块相关联,请执行以下操作。您现在可以使用 PyType_GetModuleState() 在定义类上获取模块状态,例如从模块方法。

示例来自 Modules/zlibmodule.c 。第一, defining_class 添加到诊所输入::

/*[clinic input]
zlib.Compress.compress

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

运行Argument Clinic Tool(参数诊所工具)后,会生成以下函数签名:

/*[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() 来获取模块状态。示例来自 setattro 中的槽方法 Modules/_threadmodule.c ::

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 . 自定义转换器的主要用途是,如果有一个参数使用 O& 格式化单元解析此参数意味着调用 PyArg_ParseTuple() “转换器功能”。

您的转换器类应命名为 *something*_converter . 如果名称遵循此约定,那么您的converter类将自动注册到Argument Clinic;其名称将是您使用 _converter 去掉后缀。(这是通过一个元类完成的。)

你不应该再分类 CConverter.__init__ . 相反,你应该写一个 converter_init() 功能。 converter_init() 总是接受 self 参数;之后,所有附加参数 must 仅限关键字。在Argument Clinic中传递给转换器的任何参数都将传递给 converter_init() .

还有一些其他成员 CConverter 您可能希望在子类中指定。以下是当前列表:

type

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

default

此参数的python默认值,作为python值。或者魔法值 unspecified 如果没有违约。

py_default

default 它应该以字符串的形式出现在Python代码中。或 None 如果没有违约。

c_default

default 它应该以字符串的形式出现在C代码中。或 None 如果没有违约。

c_ignored_default

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

converter

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

impl_by_reference

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

parse_by_reference

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

以下是自定义转换器的最简单示例,来自 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]*/

此块将转换器添加到名为 ssize_t . 参数声明为 ssize_t 将声明为类型 Py_ssize_t ,并将由 'O&' 格式单位,它将调用 ssize_t_converter 转换器功能。 ssize_t 变量自动支持默认值。

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

编写自定义返回转换器

编写自定义返回转换器与编写自定义转换器非常相似。但它有点简单,因为返回转换器本身要简单得多。

返回转换器必须子类 CReturnConverter . 目前还没有定制返回转换器的例子,因为它们还没有被广泛使用。如果你想写你自己的返回转换器,请阅读 Tools/clinic/clinic.py 特别是执行 CReturnConverter 以及它的所有子类。

METH_O和METH_NOARGS

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

/*[clinic input]
meth_o_sample

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

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

您仍然可以使用自转换器、返回转换器,并指定 type 对象转换器的参数 METH_O .

tp_new和tp_init函数

你可以转换 tp_newtp_init 功能。给他们起名 __new____init__ 视情况而定。笔记:

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

  • PyMethodDef #define 为这些函数生成。

  • __init__ 函数返回 int 不是 PyObject * .

  • 使用docstring作为类docstring。

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

更改和重定向诊所的输出

将诊所的输出与传统的手工编辑的C代码穿插在一起是不方便的。幸运的是,Clinic是可配置的:您可以缓冲其输出以便稍后(或更早)打印。或将其输出写入单独的文件。您还可以在诊所生成的每一行输出中添加前缀或后缀。

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

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

realm

在此上下文中,字段是诊所输出的一个子部分。例如, #define 对于 PyMethodDef 结构是一个字段,称为 methoddef_define . Clinic有七个不同的字段,可以根据功能定义输出:

docstring_prototype
docstring_definition
methoddef_define
impl_prototype
parser_prototype
parser_definition
impl_definition

所有的名字都是这样的 "<a>_<b>" 在哪里 "<a>" 表示的语义对象(解析函数、impl函数、docstring或methoddef结构)和 "<b>" 表示字段是什么类型的语句。以结尾的字段名 "_prototype" 表示该事物的前向声明,而不表示该事物的实际体/数据;以 "_definition" 用事物的身体/数据表示事物的实际定义。 ("methoddef" 很特别,它是唯一以 "_define" ,表示它是一个预处理器定义。)

目的地

目的地是诊所可以写入输出的地方。有五个内置目的地:

block

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

buffer

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

file

一个单独的“诊所文件”,由诊所自动创建。为文件选择的文件名是 {{basename}}.clinic{{extension}} 在哪里 basenameextension 被分配的输出来自 os.path.splitext() 在当前文件上运行。(例如: file 目的地 _pickle.c 会写信给 _pickle.clinic.c

重要提示:使用 file 目的地,你 必须检查 生成的文件!

two-pass

类缓冲器 buffer . 但是,一个两通缓冲区只能转储一次,它打印出在所有处理过程中发送给它的所有文本,即使是从诊所块发送的文本。 之后 倾倒点。

suppress

文本被禁止丢弃。

Clinic定义了五个新的指令,允许您重新配置其输出。

第一个新指令是 dump

dump <destination>

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

第二个新指令是 output . 最基本的形式 output 是这样的:

output <field> <destination>

这告诉诊所输出 realm目的地 . output 还支持一个特殊的元目标,称为 everything ,指示Clinic输出 all 字段到 目的地 .

output 具有许多其他功能:

output push
output pop
output preset <preset>

output pushoutput pop 允许您在内部配置堆栈上推送和弹出配置,以便临时修改输出配置,然后轻松恢复以前的配置。只需在更改前按一下以保存当前配置,然后在希望恢复以前的配置时弹出。

output preset 将诊所的输出设置为若干内置预设配置之一,如下所示:

block

诊所的原始启动配置。在输入块后立即写入所有内容。

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

file

旨在将所有内容写入“诊所文件”。那你呢 #include 此文件位于文件顶部附近。您可能需要重新排列文件以使其正常工作,尽管这通常意味着为各种 typedefPyTypeObject 定义。

压制 parser_prototypedocstring_prototype 写下 impl_definitionblock 把其他的东西都写下来 file .

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

buffer

将诊所的大部分输出保存起来,并在结尾处写入您的文件。对于实现模块或内置类型的python文件,建议将缓冲区转储到模块或内置类型的静态结构的正上方;这些缓冲区通常非常接近末尾。使用 buffer 可能需要更多的编辑 file ,如果文件具有静态 PyMethodDef 在文件中间定义的数组。

压制 parser_prototypeimpl_prototypedocstring_prototype 写下 impl_definitionblock 把其他的东西都写下来 file .

two-pass

类似于 buffer 预设,但将声明转发到 two-pass 缓冲区,以及 buffer . 这和 buffer 预设,但可能需要的编辑少于 buffer . 转储 two-pass 缓冲区靠近文件的顶部,并转储 buffer 接近尾端,就像使用 buffer 预设。

抑制 impl_prototype 写下 impl_definitionblockdocstring_prototypemethoddef_defineparser_prototypetwo-pass ,将其他所有内容写入 buffer .

partial-buffer

类似于 buffer 预设,但写入更多内容到 block ,只将生成的代码的真正大块写入 buffer . 这就避免了使用前的定义问题 buffer 总的来说,在块的输出中拥有稍微多的东西的成本很低。转储 buffer 接近尾端,就像使用 buffer 预设。

抑制 impl_prototype 写下 docstring_definitionparser_definitionbuffer ,将其他所有内容写入 block .

第三个新指令是 destination

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

这将对名为 name .

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

这个 new 子命令的工作方式如下:

destination <name> new <type>

这将创建一个具有名称的新目标 <name> 类型 <type> .

有五种目的地类型:

suppress

丢弃文本。

block

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

buffer

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

file

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

目标<name>new<type><file_template>

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

{路径}

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

{Drime}

文件所在目录的名称。

{BaseNe}}

只是文件名,不包括目录。

{basename_root}

扩展名被剪掉的basename(所有内容都包括但不包括最后一个“.”)。

{basename_extension}

最后一个“.”以及之后的所有内容。如果basename不包含句点,则这将是空字符串。

如果文件名中没有句点,basename和filename相同,扩展名为空。”basename扩展名始终与“文件名”完全相同。

two-pass

两次通过缓冲区,如上面的“两次通过”内置目的地。

这个 clear 子命令的工作方式如下:

destination <name> clear

它将删除目标中截至此点的所有累积文本。(我不知道你需要这个做什么,但我想也许在别人试验的时候它会有用。)

第四个新指令是 set

set line_prefix "string"
set line_suffix "string"

set 允许您在Clinic中设置两个内部变量。 line_prefix 是一个字符串,将在诊所的每一行输出之前进行预处理; line_suffix 是一个字符串,将附加到诊所输出的每一行。

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

{block comment start}

变成字符串 /* ,C文件的开始注释文本序列。

{block comment end}

变成字符串 */ ,C文件的结束注释文本序列。

最后一个新指令是不需要直接使用的,调用 preserve

preserve

这告诉临床,输出的当前内容应该保持不变。当将输出转储到 file 文件;将其封装在诊所块中可以让诊所使用其现有的校验和功能,以确保文件在被覆盖之前不会被手工修改。

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 结构,用生成的宏参数clinic替换它们:

MODULE_FUNCTIONNAME_METHODDEF

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

也许你在想:如果 HAVE_FUNCTIONNAME 没有定义?这个 MODULE_FUNCTIONNAME_METHODDEF 宏也不会被定义!

这就是Argument Clinic变得非常聪明的地方。它实际上检测到参数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.

发生这种情况时,只需打开文件,找到 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:...]*/