Argument Clinic How-To

作者:拉里·海斯廷斯

抽象

Argument Clinic是CPython C文件的预处理器。它的目的是自动化所有的参数编写参数解析代码为“内置”的样板。本文档介绍如何将您的第一个C函数转换为使用Argument Clinic,然后介绍一些有关Argument Clinic使用的高级主题。

当前Argument Clinic被认为是CPython的内部专用。它不支持对CPython外部的文件的使用,并且不保证未来版本的向后兼容性。换句话说:如果您为CPython维护一个外部C扩展,欢迎您在自己的代码中尝试使用Argument Clinic。但CPython 3.5 附带的Argument Clinic版本可能完全不兼容,并破坏所有代码。

The Goals Of 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时,你应该记住一个想法:你给它的信息越多,它能做的就越好。争论诊所现在比较简单。但随着它的发展,它会变得更加复杂,它应该能够做许多有趣的和聪明的事情与你提供的所有信息。

Basic Concepts And Usage

参数诊所附带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])是开始行
  • 最初注释的最后一行([诊所 开始 生成 代码] * / t0 >)是结束行
  • 最后一行(/ * [clinic end 生成 代码: checksum = .. 。] * /)是校验和行
  • 在开始行和结束行之间是输入
  • 在结束行和校验和行之间是输出
  • 从开始行到包含校验和行的所有文本是(尚未由Argument Clinic成功处理但没有输出或校验和行但仍被视为块的块)。

Converting Your First Function

了解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()有多个调用,则应该选择一个不同的函数。争论诊所支持所有这些情况。但这些都是高级主题 - 让我们做一些更简单的第一个功能。

    此外,如果函数对PyArg_ParseTuple()PyArg_ParseTupleAndKeywords()有多个调用,它支持同一个参数的不同类型,或者函数使用除PyArg_Parse函数之外的其他函数解析它的参数,它可能不适合转换到Argument Clinic。Argument Clinic不支持通用函数或多态参数。

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

    /*[clinic input]
    [clinic start generated code]*/
    
  5. 剪切文档字符串并将其粘贴到[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会抱怨。所以让我们确保它有一个。“summary”行应该是由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文件中使用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
    

    参数诊所对“默认值”的支持非常复杂;有关详细信息,请参阅the section below on default values部分。

    在参数下方添加空行。

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

    对于每个参数,从PyArg_Parse()格式参数复制该参数的“格式单位”,并将指定作为其转换器,作为带引号的字符串。(“format unit”是format参数的一到三个字符子串的正式名称,它告诉参数解析函数该变量的类型是什么以及如何转换它。有关格式单位的详情,请参阅Parsing arguments and building values。)

    对于诸如z#的多字符格式单位,请使用整个2或3字符字符串。

    样品:

     /*[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. 为每个参数编写每个参数的文档字符串是有帮助的。但是每个参数的文档字符串是可选的;如果您愿意,可以跳过此步骤。

    以下是如何添加每参数文档字符串。每个参数的文档字符串的第一行必须比参数定义缩进。第一行的左边距为整个每个参数的文档字符串建立左边距;所有你写的文本将被这个金额减少。你可以写多少文本,如果你愿意,跨多行。

    样品:

    /*[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运气一切工作,你的块现在有输出!在文本编辑器中重新打开该文件,以查看:

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

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

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

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

    Second, the format string passed in to PyArg_ParseTuple() or PyArg_ParseTupleAndKeywords() should be exactly the same as the hand-written one in the existing function, up to the colon or semi-colon.

    (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!

Advanced Topics

现在你已经有过使用Argument Clinic的经验,现在是一些高级主题的时候了。

Symbolic default values

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

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

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

(在未来,这可能需要更复杂,允许像CONSTANT - 1 。)

Renaming the C functions and variables generated by Argument Clinic

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允许你给一个参数不同的名称在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参数!

Converting functions using PyArg_UnpackTuple

要转换用PyArg_UnpackTuple()解析其参数的函数,只需写出所有参数,将每个参数指定为object您可以指定type参数来适当转换类型。所有参数都应该标记为仅位置(在最后一个参数后面的行上添加/)。

目前生成的代码将使用PyArg_ParseTuple(),但这将很快改变。

Optional Groups

一些遗留函数有一个棘手的方法来解析它们的参数:它们计算位置参数的数量,然后使用switch语句调用几个不同的PyArg_ParseTuple()(这些函数不能接受仅关键字的参数。)此方法用于在创建PyArg_ParseTupleAndKeywords()之前模拟可选参数。

虽然使用此方法的函数通常可以转换为使用PyArg_ParseTupleAndKeywords(),可选参数和默认值,但并不总是可能的。这些遗留函数中的一些具有行为PyArg_ParseTupleAndKeywords()不直接支持。最明显的例子是内置函数range(),它在其所需参数的left侧有一个可选参数!另一个例子是curses.window.addch(),它具有一组必须始终一起指定的两个参数。(The arguments are called x and y; if you call the function passing in x, you must also pass in y–and if you don’t pass in x you may not pass in y either.)

在任何情况下,Argument Clinic的目标是支持对所有现有CPython内置函数的参数解析,而无需更改其语义。因此,Argument Clinic支持使用称为可选组的此替代方法进行解析。可选组是必须全部传递在一起的参数组。它们可以位于所需参数的左侧或右侧。They can only be used with positional-only parameters.

注意

可选组只有用于转换对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函数。The parameter will be an int named group_{direction}_{number}, where {direction} is either right or left depending on whether the group is before or after the required parameters, and {number} is a monotonically increasing number (starting at 1) indicating how far away the group is from the required parameters. 当调用impl时,如果此组未使用,则此参数将设置为零,如果使用此组,则将其设置为非零。(通过使用或未使用,我的意思是参数是否在此调用接收参数。)
  • 如果没有必需的参数,那么可选组将表现为它们在所需参数的右侧。
  • 在歧义的情况下,参数解析代码倾向于左侧的参数(在所需的参数之前)。
  • 可选组只能包含仅位置参数。
  • 可选组仅适用于传统代码的请不要对新代码使用可选组。

Using real Argument Clinic converters, instead of “legacy converters”

为了节省时间,并最大限度地减少您需要学习多少,以实现您的第一个端口到Argument Clinic,上面的演练告诉您使用“旧版转换器”。“传统转换器”是一个方便,明确设计使现有代码移植到Argument Clinic更容易。并且要清楚,当移植代码为Python 3.4时,他们的使用是可以接受的。

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

  • 合适的转换器在其意图上更容易阅读和更清楚。
  • 有些格式单元不支持作为“旧版转换器”,因为它们需要参数,而旧版转换器语法不支持指定参数。
  • 在将来,我们可能有一个新的参数解析库,不限于PyArg_ParseTuple()支持;此灵活性将不适用于使用旧版转换器的参数。

因此,如果您不介意多少额外的努力,请使用正常的转换器,而不是旧版转换器。

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

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

c_default
The default value for this parameter when defined in C. Specifically, this will be the initializer for the variable declared in the “parse function”. See the section on default values for how to use this. Specified as a string.
annotation
The annotation value for this parameter. Not currently supported, because PEP 8 mandates that the Python library may not use annotations.

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

accept

A set of Python types (and possibly pseudo-types); this restricts the allowable Python argument to values of these types. (This is not a general-purpose facility; as a rule it only supports specific lists of types as shown in the legacy converter table.)

To accept None, add NoneType to this set.

bitwise
Only supported for unsigned integers. The native integer value of this Python argument will be written to the parameter without any range checking, even for negative values.
converter
Only supported by the object converter. Specifies the name of a C “converter function” to use to convert this object to a native type.
encoding
Only supported for strings. Specifies the encoding to use when converting this string from a Python str (Unicode) value into a C char * value.
subclass_of
Only supported for the object converter. Requires that the Python value be a subclass of a Python type, as expressed in C.
type
Only supported for the object and self converters. Specifies the C type that will be used to declare the variable. Default value is "PyObject *".
zeroes
Only supported for strings. If true, embedded NUL bytes ('\\0') are permitted inside the value. The length of the string will be passed in to the impl function, just after the string parameter, as a parameter named <parameter_name>_length.

请注意,并非每个可能的参数组合都可以工作。通常,这些参数由特定的PyArg_ParseTuple 格式单位实现,具有特定的行为。例如,目前您不能调用unsigned_short,也不能指定bitwise=True虽然这是完全合理的认为这将工作,这些语义不映射到任何现有的格式单位。所以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}) / t0>
'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_PY_LONG_LONG(bitwise=True)
'l'long
'L'PY_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 - 转换器即可查看完整列表。

Py_buffer

当使用Py_buffer转换器(或's*''w*''*y''z*'旧版转换器),您必须不在提供的缓冲区上调用PyBuffer_Release()Argument Clinic会生成代码(在解析函数中)。

Advanced converters

记住那些您第一次跳过的格式单元,因为它们是高级的?这里是如何处理这些。

诀窍是,所有这些格式单元都接受参数 - 转换函数,类型或指定编码的字符串。(但“旧版转换器”不支持参数。这就是为什么我们跳过他们为你的第一个功能。)The argument you specified to the format unit is now an argument to the converter; this argument is either converter (for O&), subclass_of (for O!)或encoding(对于以e开头的所有格式单位)。

当使用subclass_of时,您可能还想为object()type使用其他自定义参数,为参数。例如,如果你想确保对象是PyUnicode_Type的子类,你可能想使用转换器object(type ='PyUnicodeObject *', subclass_of ='&amp; PyUnicode_Type')

使用Argument Clinic的一个可能的问题是:从e开始的格式单元可能会有一些灵活性。当用手写一个PyArg_Parse调用时,你理论上可以在运行时决定传递给PyArg_ParseTuple()的编码字符串。但是现在这个字符串必须在Argument-Clinic-预处理时被硬编码。这个限制是故意的;它使支持这种格式单元更容易,并可能允许未来的优化。这种限制似乎不合理; CPython本身总是以格式单元以e开头的参数传递静态硬编码的编码字符串。

Parameter default values

参数的默认值可以是多个值中的任何一个。最简单的,它们可以是string,int或float字面值:

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

他们也可以使用任何Python的内建常量:

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

还特别支持默认值NULL和简单表达式,在以下各节中进行了说明。

The NULL default value

对于字符串和对象参数,可以将它们设置为None,表示没有默认值。然而,这意味着C变量将被初始化为Py_None为了方便起见,有一个特殊的值叫NULL就是这个原因:从Python的角度来看,它的行为类似于默认值None,但是C变量初始化为NULL

Expressions specified as default values

参数的默认值不仅仅是字面值值。它可以是一个整个表达式,使用数学运算符和查找对象上的属性。然而,这种支持不是很简单,因为一些非显而易见的语义。

考虑下面的例子:

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 t0 >)。
  • 自动序列分割(* [1, 2, 3])。
  • List / set / dict推导式和生成器表达式。
  • Tuple / list / set / dict字面值。

Using a return converter

默认情况下,impl函数Argument Clinic为你返回PyObject *但是你的C函数经常计算一些C类型,然后在最后一刻将它转换为PyObject *Argument Clinic处理将您的输入从Python类型转换为本地C类型 - 为什么不将它的返回值从本地C类型转换为Python类型?

这就是一个“返回转换器”。它改变你的impl函数返回一些C类型,然后添加代码到生成的(非imp)函数处理转换该值到适当的PyObject *

返回转换器的语法与参数转换器的语法相似。你指定返回转换器,就像它是一个函数本身的返回注解。返回转换器的行为与参数转换器的行为大致相同;他们接受参数,参数都是仅关键字,如果你不改变任何默认参数,你可以省略括号。

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

使用返回转换器时还有一个额外的复杂因素:如何指示发生错误?通常,函数为成功返回有效的(非NULL)指针,而对失败返回NULL但是如果你使用一个整数返回转换器,所有的整数都是有效的。如何Argument Clinic检测到错误?它的解决方案:每个返回转换器隐含地查找指示错误的特殊值。如果返回该值,并且已设置错误(PyErr_Occurred()返回true值),则生成的代码将传播错误。否则它将编码你返回的值像正常。

目前Argument Clinic仅支持几个返回转换器:

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

这些都不带参数。对于前三个,返回-1表示错误。对于DecodeFSDefault,返回类型为char *;返回NULL指针以指示错误。

(There’s also an experimental NoneType converter, which lets you return Py_None on success or NULL on failure, without having to increment the reference count on Py_None. 我不确定它增加了足够的清晰度值得使用。)

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

Cloning existing functions

如果您有许多类似的功能,您可以使用Clinic的“克隆”功能。克隆现有函数时,可以重用:

  • 其参数,包括
    • 他们的名字,
    • 其转换器,具有所有参数,
    • 其默认值,
    • 他们的每个参数的文档字符串,
    • 他们的(仅限位置,位置或关键字,或仅限关键字)
  • 其返回转换器。

唯一没有从原始函数复制的是它的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,以说明您必须使用两个函数的完整路径。)

对不起,没有用于部分克隆函数或克隆函数然后修改函数的语法。克隆是一个全有或无关的命题。

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

Calling Python code

其余的高级主题需要你编写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:...]*/

Using a “self converter”

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

重点是什么?这样可以覆盖self的类型,或者为其指定不同的默认名称。

如何指定要投放self的自定义类型?If you only have one or two functions with the same type for self, you can directly use Argument Clinic’s existing self converter, passing in the type you want to use as the type parameter:

/*[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 member:

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

Writing a custom converter

正如我们在上一节中所暗示的...你可以编写自己的转换器!转换器只是一个继承自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的转换器。Parameters declared as ssize_t will be declared as type Py_ssize_t, and will be parsed by the 'O&' format unit, which will call the ssize_t_converter converter function. ssize_t变量​​自动支持默认值。

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

Writing a custom return converter

编写自定义返回转换器很像编写自定义转换器。除了它有点简单,因为返回转换器本身更简单。

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

METH_O and METH_NOARGS

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

/*[clinic input]
meth_o_sample

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

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

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

tp_new and tp_init functions

您可以转换tp_newtp_init函数。只要将它们命名为__new____init__即可。笔记:

  • __new__生成的函数名称不会像在默认情况下那样在__new__中结束。它只是类的名称,转换为有效的C标识符。
  • 不为这些函数生成PyMethodDef #define
  • __init__函数返回int,而不是PyObject *
  • 使用docstring作为类docstring。
  • 虽然__new____init__函数必须始终接受argskwargs对象,但转换时可以指定任何声明为您喜欢的这些功能。(如果你的函数不支持关键字,生成的解析函数会抛出一个异常,如果它收到任何。

Changing and redirecting Clinic’s output

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

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

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

字段

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

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
默认目标:打印在当前Clinic块的输出部分中。
buffer
一个文本缓冲区,您可以在其中保存文本以供以后使用。此处发送的文本将附加到任何现有文本的末尾。当Clinic完成处理文件时,在缓冲区中保留任何文本是一个错误。
file

将由Clinic自动创建的单独的“诊所文件”。The filename chosen for the file is {basename}.clinic{extension}, where basename and extension were assigned the output from os.path.splitext() run on the current file. (示例:_pickle.cfile目标将写入_pickle.clinic.c。)

Important: When using a file destination, you must check in the generated file!

two-pass
buffer的缓冲区。但是,双通道缓冲区只能写入一次,并且在所有处理期间打印输出的所有文本,即使是之后的
suppress
文本被抑制 - 抛弃。

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

第一个新指令是dump

dump <destination>

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

第二个新指令是outputoutput的最基本形式如下:

output <field> <destination>

这告诉Clinic输出字段目标output还支持一个特殊的元目的地,称为everything,告诉Clinic将所有字段输出到目标

output具有许多其他功能:

output push
output pop
output preset <preset>

输出 推动输出 pop push和pop配置在内部配置堆栈,以便您可以临时修改输出配置,然后容易地恢复以前的配置。只需在更改之前按下即可保存当前配置,然后在希望还原以前的配置时弹出。

输出 预设将Clinic的输出设置为以下几种内建预设配置之一:

block

Clinic’s original starting configuration. Writes everything immediately after the input block.

Suppress the parser_prototype and docstring_prototype, write everything else to block.

file

Designed to write everything to the “clinic file” that it can. You then #include this file near the top of your file. You may need to rearrange your file to make this work, though usually this just means creating forward declarations for various typedef and PyTypeObject definitions.

Suppress the parser_prototype and docstring_prototype, write the impl_definition to block, and write everything else to file.

The default filename is "{dirname}/clinic/{basename}.h".

buffer

Save up all most of the output from Clinic, to be written into your file near the end. For Python files implementing modules or builtin types, it’s recommended that you dump the buffer just above the static structures for your module or builtin type; these are normally very near the end. Using buffer may require even more editing than file, if your file has static PyMethodDef arrays defined in the middle of the file.

Suppress the parser_prototype, impl_prototype, and docstring_prototype, write the impl_definition to block, and write everything else to file.

two-pass

Similar to the buffer preset, but writes forward declarations to the two-pass buffer, and definitions to the buffer. This is similar to the buffer preset, but may require less editing than buffer. Dump the two-pass buffer near the top of your file, and dump the buffer near the end just like you would when using the buffer preset.

Suppresses the impl_prototype, write the impl_definition to block, write docstring_prototype, methoddef_define, and parser_prototype to two-pass, write everything else to buffer.

partial-buffer

Similar to the buffer preset, but writes more things to block, only writing the really big chunks of generated code to buffer. This avoids the definition-before-use problem of buffer completely, at the small cost of having slightly more stuff in the block’s output. Dump the buffer near the end, just like you would when using the buffer preset.

Suppresses the impl_prototype, write the docstring_definition and parser_definition to buffer, write everything else to block.

第三个新指令是destination

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

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

有两个定义的子命令:newclear

new子命令的工作原理如下:

destination <name> new <type>

这将创建一个名为<name>的新目标,并输入<type>

有五种目的地类型:

suppress
Throws the text away.
block
Writes the text to the current block. This is what Clinic originally did.
buffer
A simple text buffer, like the “buffer” builtin destination above.
file

A text file. The file destination takes an extra argument, a template to use for building the filename, like so:

destination <name> new <type> <file_template>

The template can use three strings internally that will be replaced by bits of the filename:

{path}
The full path to the file, including directory and full filename.
{dirname}
The name of the directory the file is in.
{basename}
Just the name of the file, not including the directory.
{basename_root}
Basename with the extension clipped off (everything up to but not including the last ‘.’).
{basename_extension}
The last ‘.’ and everything after it. If the basename does not contain a period, this will be the empty string.

If there are no periods in the filename, {basename} and {filename} are the same, and {extension} is empty. “{basename}{extension}” is always exactly the same as “{filename}”.”

two-pass
A two-pass buffer, like the “two-pass” builtin destination above.

clear子命令的工作原理如下:

destination <name> clear

它删除目标中到此点为止的所有累积文本。(我不知道你需要什么,但我想也许这将是有用的,当有人在试验。)

第四个新指令是set

set line_prefix "string"
set line_suffix "string"

set可让您在Clinic中设置两个内部变量。line_prefix是将被添加到Clinic输出的每一行的字符串; line_suffix是一个字符串,将附加到Clinic输出的每一行。

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

{block comment start}
Turns into the string /*, the start-comment text sequence for C files.
{block comment end}
Turns into the string */, the end-comment text sequence for C files.

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

preserve

这告诉Clinic,输出的当前内容应该保持不变。当将输出转储到file文件中时,Clinic内部使用这种方法;将其包装在Clinic块中可以使Clinic使用其现有的校验和功能,以确保文件在被覆盖之前不被手动修改。

The #ifdef trick

如果你正在转换一个不是在所有平台上都可用的功能,有一个窍门,你可以使用,使生活更容易一点。现有代码可能如下所示:

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

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

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

在这种情况下,您应该在#ifdef中包含impl函数的主体,如下所示:

#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将额外的代码写入“缓冲​​区”目标。这可能意味着你从争议诊所得到投诉:

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

When this happens, just open your file, find the dump buffer block that Argument Clinic added to your file (it’ll be at the very bottom), then move it above the PyMethodDef structure where that macro is used.

Using Argument Clinic in Python files

实际上可以使用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:...]*/