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的代码,您确实应该使用文档中稍后将看到的一些高级概念(如“返回转换器”和“自转换器”),进一步进行转换。但我们将保持简单的演练,以便您可以学习。
我们潜水吧!
确保您正在使用新更新的cpython主干结帐。
找到一个调用
PyArg_ParseTuple()或PyArg_ParseTupleAndKeywords(),并且还没有转换为与Argument Clinic一起工作。例如,我正在使用_pickle.Pickler.dump().如果调用给
PyArg_Parse函数使用以下任何格式单位:O& O! es es# et et#
或者如果它有多个调用
PyArg_ParseTuple(),您应该选择一个不同的函数。Argument Clinic does 支持所有这些场景。但是这些是高级主题,让我们为您的第一个函数做一些简单的事情。此外,如果函数对
PyArg_ParseTuple()或PyArg_ParseTupleAndKeywords()如果它对同一个参数支持不同的类型,或者如果该函数使用Pyarg_Parse函数之外的某个函数来解析其参数,那么它可能不适合转换为Argument Clinic。Argument Clinic不支持泛型函数或多态参数。在函数上方添加以下样板文件,创建块:
/*[clinic input] [clinic start generated code]*/
剪切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]*/
如果你的docstring没有“摘要”行,Argument Clinic会抱怨。所以让我们确定它有一个。“摘要”行应该是一个段落,在docstring的开头由一个80列的行组成。
(我们的示例docstring只包含一个摘要行,因此不必为此步骤更改示例代码。)
在docstring上方,输入函数的名称,后跟一行空白。这应该是函数的python名称,并且应该是函数的完整虚线路径,它应该以模块的名称开始,包括任何子模块,如果函数是一个类上的方法,它也应该包括类名。
样品:
/*[clinic input] _pickle.Pickler.dump Write a pickled representation of obj to the open file. [clinic start generated code]*/
如果这是该模块或类第一次在此C文件中与参数clinic一起使用,则必须声明该模块和/或类。正确的Argument Clinic卫生更类似于在靠近C文件顶部的单独块中声明这些,就像在顶部包含文件和静态数据一样。(在示例代码中,我们将只显示相邻的两个块。)
类和模块的名称应该与Python看到的名称相同。检查中定义的名称
PyModuleDef或PyTypeObject适当时。声明类时,还必须在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]*/
向函数声明每个参数。每个参数都应该有自己的行。所有参数行都应该从函数名和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]*/
如果你的功能
|在格式字符串中,意味着某些参数具有默认值,您可以忽略它。Argument Clinic根据参数是否具有默认值来推断哪些参数是可选的。如果你的功能
$在格式字符串中,这意味着它只接受关键字参数,请指定*在第一个只包含关键字的参数前面的行上,缩进与参数行相同。(
_pickle.Pickler.dump两者都没有,所以我们的样本不变。)如果现有的C函数调用
PyArg_ParseTuple()(而不是PyArg_ParseTupleAndKeywords(),那么它的所有参数都只是位置的。若要仅在Argument Clinic中将所有参数标记为位置,请添加
/在最后一个参数后的行上,缩进与参数行相同。目前,这是全部或全部;要么所有参数都只是位置参数,要么都不是。(在未来的参数中,诊所可能会放宽这一限制。)
样品:
/*[clinic input] module _pickle class _pickle.Pickler "PicklerObject *" "&Pickler_Type" [clinic start generated code]*/ /*[clinic input] _pickle.Pickler.dump obj: 'O' / Write a pickled representation of obj to the open file. [clinic start generated code]*/
为每个参数编写一个每个参数的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]*/
保存并关闭文件,然后运行
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"
仔细检查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 相同的。注意,它输出的最后一行是“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; ...
记住宏
PyMethodDef此函数的结构?查找现有PyMethodDef此函数的结构,并将其替换为对宏的引用。(如果builtin在模块范围内,这可能非常接近文件的结尾;如果builtin是类方法,这可能在下面,但相对接近实现。)请注意,宏体包含一个尾随逗号。所以当您替换现有的静态
PyMethodDef结构与宏, 不要 在末尾加一个逗号。样品:
static struct PyMethodDef Pickler_methods[] = { __PICKLE_PICKLER_DUMP_METHODDEF __PICKLE_PICKLER_CLEAR_MEMO_METHODDEF {NULL, NULL} /* sentinel */ };
编译,然后运行回归测试套件的相关部分。此更改不应引入任何新的编译时警告或错误,并且不应对Python的行为进行任何外部可见的更改。
好吧,除了一个区别:
inspect.signature()运行函数现在应该提供一个有效的签名!恭喜您,您已将第一个功能移植到Argument Clinic!
高级主题¶
现在你已经有了一些与Argument Clinic合作的经验,是时候讨论一些高级话题了。
符号默认值¶
为参数提供的默认值不能是任何任意表达式。目前明确支持以下内容:
数字常量(整数和浮点)
字符串常量
True,False, andNone简单的符号常量
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() ,它有一组必须始终一起指定的两个参数。(参数被调用 x 和 y ;如果调用传入的函数 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}}要么是right或left取决于组是在所需参数之前还是之后,以及{{number}}是一个单调递增的数字(从1开始),表示组与所需参数的距离。调用IMPL时,如果未使用此组,则此参数将设置为零;如果使用了此组,则此参数将设置为非零。(对于已用或未用,我的意思是参数是否在这个调用中接收到参数。)如果没有必需的参数,可选组的行为就好像它们在必需参数的右边一样。
在歧义的情况下,参数解析代码倾向于左边的参数(在所需参数之前)。
可选组只能包含位置参数。
可选组包括 only 用于旧代码。请不要为新代码使用可选组。
使用实参诊所转换器,而不是“传统转换器”¶
为了节省时间,并尽量减少实现第一个参数转换诊所所需的学习量,上面的演练告诉您使用“遗留转换器”。传统转换器“是一种方便的工具,其设计明确地使将现有代码移植到Argument Clinic变得更容易。而且要清楚的是,在为python 3.4移植代码时,它们的使用是可以接受的。
但是,从长远来看,我们可能希望所有的块都使用ArgumentClinic的转换器真正语法。为什么?有几个原因:
正确的转换器更容易阅读,在其意图上更清晰。
有些格式单元不支持作为“传统转换器”,因为它们需要参数,而传统转换器语法不支持指定参数。
将来,我们可能会有一个新的参数解析库,它不局限于
PyArg_ParseTuple()支持;这种灵活性不适用于使用传统转换器的参数。
因此,如果您不介意做一些额外的工作,请使用普通转换器而不是传统转换器。
简而言之,Argument Clinic(非遗留)转换器的语法看起来像是一个python函数调用。但是,如果函数没有显式参数(所有函数都采用其默认值),则可以省略括号。因此 bool 和 bool() 是完全相同的转换器。
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仅支持
object和self转换器。指定将用于声明变量的C类型。默认值为"PyObject *".zeroes仅支持字符串。如果为真,则嵌入nul字节 (
'\\0')允许在值内。字符串的长度将作为名为<parameter_name>_length.
请注意,并非所有可能的参数组合都有效。通常这些参数是由特定的 PyArg_ParseTuple 格式化单元 具有特定行为。例如,当前不能调用 unsigned_short 无需说明 bitwise=True . 虽然认为这样做是完全合理的,但是这些语义并没有映射到任何现有的格式单元。所以Argument Clinic不支持它。(或者至少还没有。)
下表显示了传统转换器到实参诊所转换器的映射。左边是旧版转换器,右边是要替换的文本。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
举个例子,这是我们的示例 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_defaultdefault它应该以字符串的形式出现在Python代码中。或None如果没有违约。c_defaultdefault它应该以字符串的形式出现在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_new 和 tp_init 功能。给他们起名 __new__ 或 __init__ 视情况而定。笔记:
为生成的函数名
__new__不会结束__new__就像默认情况下那样。它只是类的名称,转换为有效的C标识符。不
PyMethodDef#define为这些函数生成。__init__函数返回int不是PyObject *.使用docstring作为类docstring。
虽然
__new__和__init__函数必须始终接受args和kwargs对象,在转换时,可以为这些函数指定您类似于的任何签名。(如果函数不支持关键字,则生成的解析函数在收到任何异常时将引发异常。)
更改和重定向诊所的输出¶
将诊所的输出与传统的手工编辑的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}}在哪里basename和extension被分配的输出来自os.path.splitext()在当前文件上运行。(例如:file目的地_pickle.c会写信给_pickle.clinic.c)重要提示:使用
file目的地,你 必须检查 生成的文件!two-pass类缓冲器
buffer. 但是,一个两通缓冲区只能转储一次,它打印出在所有处理过程中发送给它的所有文本,即使是从诊所块发送的文本。 之后 倾倒点。suppress文本被禁止丢弃。
Clinic定义了五个新的指令,允许您重新配置其输出。
第一个新指令是 dump :
dump <destination>
这会将指定目标的当前内容转储到当前块的输出中,并清空它。这只适用于 buffer 和 two-pass 目的地。
第二个新指令是 output . 最基本的形式 output 是这样的:
output <field> <destination>
这告诉诊所输出 realm 到 目的地 . output 还支持一个特殊的元目标,称为 everything ,指示Clinic输出 all 字段到 目的地 .
output 具有许多其他功能:
output push
output pop
output preset <preset>
output push 和 output pop 允许您在内部配置堆栈上推送和弹出配置,以便临时修改输出配置,然后轻松恢复以前的配置。只需在更改前按一下以保存当前配置,然后在希望恢复以前的配置时弹出。
output preset 将诊所的输出设置为若干内置预设配置之一,如下所示:
block诊所的原始启动配置。在输入块后立即写入所有内容。
压制
parser_prototype和docstring_prototype,将其他所有内容写入block.file旨在将所有内容写入“诊所文件”。那你呢
#include此文件位于文件顶部附近。您可能需要重新排列文件以使其正常工作,尽管这通常意味着为各种typedef和PyTypeObject定义。压制
parser_prototype和docstring_prototype写下impl_definition到block把其他的东西都写下来file.默认文件名为
"{{dirname}}/clinic/{{basename}}.h".buffer将诊所的大部分输出保存起来,并在结尾处写入您的文件。对于实现模块或内置类型的python文件,建议将缓冲区转储到模块或内置类型的静态结构的正上方;这些缓冲区通常非常接近末尾。使用
buffer可能需要更多的编辑file,如果文件具有静态PyMethodDef在文件中间定义的数组。压制
parser_prototype,impl_prototype和docstring_prototype写下impl_definition到block把其他的东西都写下来file.two-pass类似于
buffer预设,但将声明转发到two-pass缓冲区,以及buffer. 这和buffer预设,但可能需要的编辑少于buffer. 转储two-pass缓冲区靠近文件的顶部,并转储buffer接近尾端,就像使用buffer预设。抑制
impl_prototype写下impl_definition到block写docstring_prototype,methoddef_define和parser_prototype到two-pass,将其他所有内容写入buffer.partial-buffer类似于
buffer预设,但写入更多内容到block,只将生成的代码的真正大块写入buffer. 这就避免了使用前的定义问题buffer总的来说,在块的输出中拥有稍微多的东西的成本很低。转储buffer接近尾端,就像使用buffer预设。抑制
impl_prototype写下docstring_definition和parser_definition到buffer,将其他所有内容写入block.
第三个新指令是 destination :
destination <name> <command> [...]
这将对名为 name .
有两个已定义的子命令: new 和 clear .
这个 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:...]*/