Argument Clinic How-To — Python 文档

来自菜鸟教程
Python/docs/3.7/howto/clinic
跳转至:导航、​搜索

论证诊所操作指南

作者
拉里·黑斯廷斯

抽象的

Argument Clinic 是 CPython C 文件的预处理器。 它的目的是自动化所有涉及为“内置”编写参数解析代码的样板。 本文档向您展示了如何将您的第一个 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 附带 CPython; 你会在 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=...]*/) 是 校验和行
  • 在起始线和结束线之间是 输入
  • 在结束线和校验和线之间是 输出
  • 从起始行到校验和行的所有文本统称为 。 (一个还没有被 Argument Clinic 成功处理的块没有输出或校验和行,但它仍然被认为是一个块。)


转换你的第一个函数

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

让我们潜入吧!

  1. 确保您正在使用 CPython 主干的最新更新签出。

  2. 找到一个调用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 的 Python 内置函数,并且尚未转换为与 Argument Clinic 一起使用。 对于我的示例,我使用 _pickle.Pickler.dump()

  3. 如果对 PyArg_Parse 函数的调用使用以下任一格式单位:

    O&
    O!
    es
    es#
    et
    et#

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

    此外,如果该函数多次调用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords(),其中它支持同一参数的不同类型,或者该函数使用除 PyArg_Parse 函数之外的其他函数来解析其争论,它可能不适合转换为争论诊所。 Argument Clinic 不支持泛型函数或多态参数。

  4. 在函数上方添加以下样板,创建我们的块:

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

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

    样本:

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

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

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

    样本:

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

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

    name_of_parameter: converter

    如果参数有默认值,则在转换器后添加:

    name_of_parameter: converter = default_value

    Argument Clinic 对“默认值”的支持非常复杂; 有关更多信息,请参阅 下面关于默认值 的部分。

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

    什么是“转换器”? 它建立了 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 中将所有参数标记为仅位置参数,请在最后一个参数后的一行上单独添加 /,缩进与参数行相同。

    目前这是全有或全无; 要么所有参数都是位置参数,要么都不是。 (将来 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. 为每个参数编写每个参数的文档字符串会很有帮助。 但是每个参数的文档字符串是可选的; 如果您愿意,可以跳过此步骤。

    这是添加每个参数的文档字符串的方法。 每个参数的文档字符串的第一行必须比参数定义缩进得更远。 第一行的左边距为整个每个参数的文档字符串建立左边距; 您编写的所有文本都将缩小此数量。 如果您愿意,您可以跨多行编写任意数量的文本。

    样本:

    /*[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. 仔细检查 Argument Clinic 生成的参数解析代码是否与现有代码基本相同。

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

    其次,传入 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,直到它们 ' 相同。

  15. 请注意,其输出的最后一行是“impl”函数的声明。 这是内置的实现的地方。 删除您正在修改的函数的现有原型,但保留左花括号。 现在删除它的参数解析代码和它转储参数的所有变量的声明。 注意 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 结构并将其替换为对宏的引用。 (如果内置是在模块范围内,这可能会非常接近文件的末尾;如果内置是类方法,这可能会低于但相对接近实现。)

    请注意,宏的正文包含一个尾随逗号。 因此,当您用宏替换现有的静态 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 的一些经验,现在是时候学习一些高级主题了。

符号默认值

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

  • 数字常量(整数和浮点数)
  • 字符串常量
  • TrueFalseNone
  • 简单的符号常量,如 sys.maxsize,必须以模块名称开头

如果您好奇,这在 Lib/inspect.py 中的 from_builtin() 中实现。

(在未来,这可能需要更加精细,以允许像 CONSTANT - 1 这样的完整表达式。)


重命名 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(),它在其必需参数的 left 侧有一个可选参数! 另一个例子是 curses.window.addch(),它有一组必须始终一起指定的两个参数。 (参数称为 xy;如果调用传入 x 的函数,则还必须传入 y——如果不t 传入 x 你也不能传入 y。)

无论如何,Argument Clinic 的目标是支持所有现有 CPython 内置函数的参数解析,而不改变它们的语义。 因此,Argument Clinic 支持这种解析的替代方法,使用所谓的 可选组 。 可选组是必须一起传入的参数组。 它们可以位于所需参数的左侧或右侧。 它们可以 only 与仅位置参数一起使用。

笔记

可选组是 only,用于转换多次调用 PyArg_ParseTuple() 的函数! 使用 任何 其他方法解析参数的函数应该 几乎从不 使用可选组转换为参数诊所。 使用可选组的函数目前在 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 时,如果该组未使用,则此参数将设置为零,如果使用该组,则设置为非零。 (使用或未使用,我的意思是参数是否在此调用中接收到参数。)
  • 如果没有必需的参数,可选组的行为就好像它们位于必需参数的右侧。
  • 在歧义的情况下,参数解析代码偏向于左侧的参数(在所需参数之前)。
  • 可选组只能包含仅位置参数。
  • 可选组是 only 用于遗留代码。 请不要为新代码使用可选组。


使用真正的 Argument Clinic 转换器,而不是“传统转换器”

为了节省时间,并最大限度地减少实现第一个 Argument Clinic 移植所需的学习量,上面的演练告诉您使用“传统转换器”。 “传统转换器”是一种便利,旨在使将现有代码更容易地移植到 Argument Clinic。 需要明确的是,在为 Python 3.4 移植代码时,它们的使用是可以接受的。

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

  • 合适的转换器更容易阅读,而且其意图也更清晰。
  • 有一些格式单元不被支持为“传统转换器”,因为它们需要参数,而传统转换器语法不支持指定参数。
  • 将来我们可能会有一个新的参数解析库,它不限于 PyArg_ParseTuple() 支持的内容; 这种灵活性不适用于使用传统转换器的参数。

因此,如果您不介意额外的努力,请使用普通转换器而不是传统转换器。

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

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

c_default
在 C 中定义时此参数的默认值。 具体来说,这将是“解析函数”中声明的变量的初始值设定项。 请参阅 关于默认值 的部分,了解如何使用它。 指定为字符串。
annotation
此参数的注释值。 当前不支持,因为 PEP 8 要求 Python 库不能使用注解。


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

accept

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

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

bitwise

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

converter

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

encoding

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

subclass_of

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

type

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

zeroes

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


请注意,并非所有可能的参数组合都有效。 通常这些参数由特定的 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' 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]*/

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 调用时,理论上您可以在运行时决定将什么编码字符串传递给 PyArg_ParseTuple()。 但是现在这个字符串必须在 Argument-Clinic-preprocessing-time 进行硬编码。 这种限制是故意的; 它使支持此格式单元变得更加容易,并且可能允许将来进行优化。 这种限制似乎并不无道理; CPython 本身总是为格式单元以 e 开头的参数传递静态硬编码编码字符串。


参数默认值

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

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

仅在运行时评估默认值意味着 Argument Clinic 无法计算正确的等效 C 默认值。 所以你需要明确地告诉它。 使用表达式时,还必须在 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])。
  • 列表/设置/字典理解和生成器表达式。
  • 元组/列表/设置/字典文字。


使用返回转换器

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

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

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

(如果您同时使用 "as" 返回转换器用于您的函数,则 "as" 应位于返回转换器之前。)

使用返回转换器时还有一个额外的复杂问题:您如何指示发生了错误? 通常,函数返回一个有效的(非NULL)指针表示成功,而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 *; 返回 NULL 指针以指示错误。

(还有一个实验性的 NoneType 转换器,它可以让您在成功时返回 Py_None 或在失败时返回 NULL,而无需增加 Py_None 上的引用计数。 我不确定它是否增加了足够的清晰度以值得使用。)

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


克隆现有函数

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

  • 它的参数,包括
    • 他们的名字,
    • 他们的转换器,带有所有参数,
    • 它们的默认值,
    • 他们的每个参数的文档字符串,
    • 他们的 kind(无论是仅位置、位置或关键字,还是仅关键字),以及
  • 它的返回转换器。

唯一没有从原始函数中复制的是它的文档字符串; 语法允许您指定新的文档字符串。

这是克隆函数的语法:

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

编写自定义转换器

正如我们在上一节中所暗示的……您可以编写自己的转换器! 转换器只是一个继承自 CConverter 的 Python 类。 自定义转换器的主要目的是如果您有一个使用 O& 格式单元的参数——解析这个参数意味着调用一个 PyArg_ParseTuple()“转换器函数”。

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

你不应该继承 CConverter.__init__。 相反,您应该编写一个 converter_init() 函数。 converter_init() 总是接受一个 self 参数; 之后,所有附加参数 必须 仅限关键字。 在 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
一个布尔值。 如果为 true,则 Argument Clinic 将在将其传递到 impl 函数时在变量名称前添加 &
parse_by_reference
一个布尔值。 如果为 true,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]*/

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

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


编写自定义返回转换器

编写自定义返回转换器很像编写自定义转换器。 除了它稍微简单一些,因为返回转换器本身要简单得多。

返回转换器必须是 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 转换函数,只需不要指定任何参数。

您仍然可以使用自转换器、返回转换器,并为 METH_O 的目标转换器指定 type 参数。


tp_new 和 tp_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 函数、docstring 或 methoddef 结构)和 "<b>"表示该字段是哪种语句。 以 "_prototype" 结尾的字段名称代表该事物的前向声明,没有该事物的实际主体/数据; 以 "_definition" 结尾的字段名称代表事物的实际定义,以及事物的主体/数据。 ("methoddef"比较特殊,是唯一以"_define"结尾的,代表它是一个预处理器#define。)

目的地

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

block

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

buffer

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

file

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

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

two-pass

buffer 这样的缓冲区。 但是,两遍缓冲区只能转储一次,它会打印出所有处理过程中发送给它的所有文本,即使是在转储点 之后的诊所块 也是如此。

suppress

文本被压制——扔掉。

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

第一个新指令是 dump

dump <destination>

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

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

output <field> <destination>

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

output 还有许多其他功能:

output push
output pop
output preset <preset>

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

output preset 将 Clinic 的输出设置为几种内置预设配置之一,如下所示:

block

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

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

file

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

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

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

buffer

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

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

two-pass

类似于 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> 的新目的地。

有五种目的地类型:

suppress

扔掉文本。

block

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

buffer

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

file

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

目的地新的

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

{path}

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

{dirname}

文件所在目录的名称。

{basename}

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

{basename_root}

删除扩展名的基本名称(包括最后一个 '.' 之前的所有内容,但不包括最后一个 '.')。

{basename_extension}

最后 '。' 以及之后的一切。 如果 basename 不包含句点,这将是空字符串。


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

two-pass

两遍缓冲区,如上面的“两遍”内置目标。


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