作者
Larry Hastings
摘要
Argument Clinic 是 Cpython 的一个 C 文件预处理器。旨在自动处理所有与“内置”参数解析有关的代码。本文展示了将 C 函数转换为配合 Argument Clinic 工作的做法,还介绍了一些关于 Argument Clinic 用法的进阶内容。
目前 Argument Clinic 视作仅供 CPython 内部使用。不支持在 CPython 之外的文件中使用,也不保证未来版本会向下兼容。换句话说:如果维护的是 CPython 的外部 C 语言扩展,欢迎在自己的代码中试用 Argument Clinic。但 Argument Clinic 与新版 CPython 中的版本 可能 完全不兼容,且会打乱全部代码。
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 会扫描 C 文件,精确查找以下代码:
/*[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 用到的术语:
注释的第一行 /*[clinic input]
是 起始行 。
注释([clinic start generated code]*/
)的最后一行是 结束行。
最后一行(/*[clinic end generated code: checksum=...]*/
)是 校验和行 。
在起始行和结束行之间是 输入数据。
在结束行和校验和行之间是 输出数据 。
从开始行到校验和行的所有文本,都是 块。(Argument Clinic 尚未处理成功的块,没有输出或校验和行,但仍视作一个块)。
要想了解 Argument Clinic 是如何工作的,最好的方式就是转换一个函数与之合作。下面介绍需遵循的最基本步骤。请注意,若真的准备在 CPython 中进行检查,则应进行更深入的转换,使用一些本文后续会介绍到的高级概念(比如 “返回转换” 和 “自转换”)。但以下例子将维持简单,以供学习。
就此开始
请确保 CPython 是最新的已签出版本。
找到一个调用 PyArg_ParseTuple() 或 PyArg_ParseTupleAndKeywords() ,且未被转换为采用 Argument Clinic 的 Python 内置程序。这里用了 _pickle.Pickler.dump()
。
如果对 PyArg_Parse
函数的调用采用了以下格式化单元:
O&
O!
es
es#
et
et#
或者多次调用 PyArg_ParseTuple(),则应再选一个函数。Argument Clinic 确实 支持上述这些状况。 但这些都是高阶内容——第一次就简单一些吧。
此外,如果多次调用 PyArg_ParseTuple() 或 PyArg_ParseTupleAndKeywords() 且同一参数需支持不同的类型,或者用到 PyArg_Parse 以外的函数来解析参数,则可能不适合转换为 Argument Clinic 形式。 Argument Clinic 不支持通用函数或多态参数。
在函数上方添加以下模板,创建块:
/*[clinic input]
[clinic start generated code]*/
剪下文档字符串并粘贴到 [clinic]
行之间,去除所有的无用字符,使其成为一个正确引用的 C 字符串。最有应该只留下带有左侧缩进的文本,且行宽不大于 80 个字符。(参数 Clinic 将保留文档字符串中的缩进。)
如果文档字符串的第一行看起来像是函数的签名,就把这一行去掉吧。((文档串不再需要用到它——将来对内置函数调用 help()
时,第一行将根据函数的签名自动建立。)
示例:
/*[clinic input]
Write a pickled representation of obj to the open file.
[clinic start generated code]*/
如果文档字符串中没有“摘要”行,Argument Clinic 会报错。所以应确保带有摘要行。 “摘要”行应为在文档字符串开头的一个段落,由一个80列的单行构成。
(示例的文档字符串只包括一个摘要行,所以示例代码这一步不需改动)。
在文档字符串上方,输入函数的名称,后面是空行。这应是函数的 Python 名称,而且应是句点分隔的完整路径——以模块的名称开始,包含所有子模块名,若函数为类方法则还应包含类名。
示例:
/*[clinic input]
_pickle.Pickler.dump
Write a pickled representation of obj to the open file.
[clinic start generated code]*/
如果是第一次在此 C 文件中用到 Argument Clinic 的模块或类,必须对其进行声明。清晰的 Argument Clinic 写法应于 C 文件顶部附近的某个单独块中声明这些,就像 include 文件和 statics 放在顶部一样。(在这里的示例代码中,将这两个块相邻给出。)
类和模块的名称应与暴露给 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]*/
声明函数的所有参数。每个参数都应另起一行。所有的参数行都应对齐函数名和文档字符串进行缩进。
这些参数行的常规形式如下:
name_of_parameter: converter
如果参数带有缺省值,请加在转换器之后:
name_of_parameter: converter = default_value
Argument Clinic 对 “缺省值” 的支持方式相当复杂;更多信息请参见 关于缺省值的部分 。
在参数行下面添加一个空行。
What’s a “converter”? It establishes both the type of the variable used in C, and the method to convert the Python value into a C value at runtime. For now you’re going to use what’s called a “legacy converter”—a convenience syntax intended to make porting old code into Argument Clinic easier.
每个参数都要从``PyArg_Parse()`` 格式参数中复制其 “格式单元”,并以带引号字符串的形式指定其转换器。(“格式单元”是 format
参数的1-3个字符的正式名称,用于让参数解析函数知晓该变量的类型及转换方法。关于格式单位的更多信息,请参阅 解析参数并构建值变量 )。
对于像 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]*/
如果函数的格式字符串包含 |
,意味着有些参数带有缺省值,这可以忽略。Argument Clinic 根据参数是否有缺省值来推断哪些参数是可选的。
如果函数的格式字符串中包含 $,意味着只接受关键字参数,请在第一个关键字参数之前单独给出一行 *
,缩进与参数行对齐。
(_pickle.Pickler.dump
两种格式字符串都没有,所以这里的示例不用改动。)
如果 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]*/
为每个参数都编写一个文档字符串,这很有意义。但这是可选项;可以跳过这一步。
下面介绍如何添加逐参数的文档字符串。逐参数文档字符串的第一行必须比参数定义多缩进一层。第一行的左边距即确定了所有逐参数文档字符串的左边距;所有文档字符串文本都要同等缩进。文本可以用多行编写。
示例:
/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
[clinic start generated code]*/
/*[clinic input]
_pickle.Pickler.dump
obj: 'O'
The object to be pickled.
/
Write a pickled representation of obj to the open file.
[clinic start generated code]*/
保存并关闭该文件,然后运行 Tools/clinic/clinic.py
。 运气好的话就万事大吉——程序块现在有了输出信息,并且生成了一个 .c.h
文件!在文本编辑器中重新打开该文件,可以看到:
/*[clinic input]
_pickle.Pickler.dump
obj: 'O'
The object to be pickled.
/
Write a pickled representation of obj to the open file.
[clinic start generated code]*/
static PyObject *
_pickle_Pickler_dump(PicklerObject *self, PyObject *obj)
/*[clinic end generated code: output=87ecad1261e02ac7 input=552eb1c0f52260d9]*/
显然,如果 Argument Clinic 未产生任何输出,那是因为在输入信息中发现了错误。继续修正错误并重试,直至 Argument Clinic 正确地处理好文件。
为了便于阅读,大部分“胶水”代码已写入 .c.h
文件中。需在原 .c
文件中包含这个文件,通常是在 clinic 模块之后:
#include "clinic/_pickle.c.h"
请仔细检查 Argument Clinic 生成的参数解析代码,是否与原有代码基本相同。
首先,确保两种代码使用相同的参数解析函数。原有代码必须调用 PyArg_ParseTuple() 或 PyArg_ParseTupleAndKeywords() ;确保 Argument Clinic 生成的代码调用 完全 相同的函数。
其次,传给 PyArg_ParseTuple() 或 PyArg_ParseTupleAndKeywords() 的格式字符串应该 完全 与原有函数中的相同,直到冒号或分号为止。
(Argument Clinic 生成的格式串一定是函数名后跟着 :
。如果现有代码的格式串以 ;
结尾,这种改动不会影响使用,因此不必担心。)
第三,如果格式单元需要指定两个参数(比如长度、编码字符串或指向转换函数的指针),请确保第二个参数在两次调用时 完全 相同。
第四,在输出部分会有一个预处理器宏,为该内置函数定义合适的静态 PyMethodDef 结构:
#define __PICKLE_PICKLER_DUMP_METHODDEF \
{"dump", (PyCFunction)__pickle_Pickler_dump, METH_O, __pickle_Pickler_dump__doc__},
此静态结构应与本内置函数现有的静态结构 PyMethodDef 完全 相同。
只要上述这几点存在不一致,请调整 Argument Clinic 函数定义,并重新运行 Tools/clinic/clinic.py
,直至 完全 相同。
注意,输出部分的最后一行是“实现”函数的声明。也就是该内置函数的实现代码所在。删除需要修改的函数的现有原型,但保留开头的大括号。再删除其参数解析代码和输入变量的所有声明。注意现在 Python 所见的参数即为此实现函数的参数;如果实现代码给这些变量采用了不同的命名,请进行修正。
因为稍显怪异,所以还是重申一下。现在的代码应该如下所示:
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 结构,并替换为宏的引用。(如果函数是模块级的,可能会在文件的末尾附近;如果函数是个类方法,则可能会在靠近实现代码的下方。)
注意,宏尾部带有一个逗号。所以若用宏替换已有的静态结构 PyMethodDef 时,请勿 在结尾添加逗号了。
示例:
static struct PyMethodDef Pickler_methods[] = {
__PICKLE_PICKLER_DUMP_METHODDEF
__PICKLE_PICKLER_CLEAR_MEMO_METHODDEF
{NULL, NULL} /* sentinel */
};
Compile, then run the relevant portions of the regression-test suite. This change should not introduce any new compile-time warnings or errors, and there should be no externally visible change to Python’s behavior.
差别只有一个,即 inspect.signature()
运行于新的函数上,现在应该新提供一个有效的签名!
祝贺你,现在已经用 Argument Clinic 移植了第一个函数。
现在 Argument Clinic 的使用经验已具备了一些,该介绍一些高级内容了。
提供给参数的默认值不能是表达式。目前明确支持以下形式:
数值型常数(整数和浮点数)。
字符串常量
True
、 False
和 None
。
以模块名开头的简单符号常量,如 sys.maxsize
。
(未来可能需要加以细化,以便可以采用 CONSTANT - 1
之类的完整表达式。)
Argument Clinic 会自动为其生成的函数命名。如果生成的名称与现有的 C 函数冲突,这偶尔可能会造成问题,有一个简单的解决方案:覆盖 C 函数的名称。只要在函数声明中加入关键字 "as"
,然后再加上要使用的函数名。Argument Clinic 将以该函数名为基础作为(生成的)函数名,然后在后面加上 "_impl"
,并用作实现函数的名称。
例如,若对 pickle.Pickler.dump
生成的 C 函数进行重命名,应如下所示:
/*[clinic input]
pickle.Pickler.dump as pickler_dumper
...
原函数会被命名为 pickler_dumper()
,而实现函数现在被命名为``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() 解析其参数,只需写出所有参数,并将每个参数定义为 object
。可以指定 type
参数,以便能转换为合适的类型。所有参数都应标记为只认位置(在最后一个参数后面加上 /
)。
目前,所生成的代码将会用到 PyArg_ParseTuple() ,但很快会做出改动。
有些过时的函数用到了一种让人头疼的函数解析方式:计算位置参数的数量,据此用 switch
语句进行各个不同的 PyArg_ParseTuple() 调用。(这些函数不能接受只认关键字的参数。)在没有 PyArg_ParseTupleAndKeywords() 之前,这种方式曾被用于模拟可选参数。
虽然这种函数通常可以转换为采用 PyArg_ParseTupleAndKeywords() 、可选参数和默认值的方式,但并不是全都可以做到。这些过时函数中, PyArg_ParseTupleAndKeywords() 并不能直接支持某些功能。最明显的例子是内置函数 range()
,它的必需参数的 左 边存在一个可选参数!另一个例子是 curses.window.addch()
,它的两个参数是一组,必须同时指定。(参数名为 x
和 y
;如果调用函数时传入了 x
,则必须同时传入``y``;如果未传入 x
,则也不能传入 y
)。
不管怎么说,Argument Clinic 的目标就是在不改变语义的情况下支持所有现有 CPython 内置参数的解析。因此,Argument Clinic 采用所谓的 可选组 方案来支持这种解析方式。可选组是必须一起传入的参数组。他们可以在必需参数的左边或右边,只能 用于只认位置的参数。
备注
可选组 仅 适用于多次调用 PyArg_ParseTuple() 的函数!采用 任何 其他方式解析参数的函数,应该 几乎不 采用可选组转换为 Argument Clinic 解析。目前,采用可选组的函数在 Python 中无法获得准确的签名,因为 Python 不能理解这个概念。请尽可能避免使用可选组。
若要定义可选组,可在要分组的参数前面加上 [
,在这些参数后加上``]`` ,要在同一行上。举个例子,下面是 curses.window.addch
采用可选组的用法,前两个参数和最后一个参数可选:
/*[clinic input]
curses.window.addch
[
x: int
X-coordinate.
y: int
Y-coordinate.
]
ch: object
Character to add.
[
attr: long
Attributes for the character.
]
/
...
注:
每一个可选组,都会额外传入一个代表分组的参数。 参数为 int 型,名为 group_{direction}_{number}
,其中 {direction}
取决于此参数组位于必需参数 right
还是 left
,而 {number}
是一个递增数字(从 1 开始),表示此参数组与必需参数之间的距离。 在调用函数时,若未用到此参数组则此参数将设为零,若用到了参数组则该参数为非零。 所谓的用到或未用到,是指在本次调用中形参是否收到了实参。
如果不存在必需参数,可选组的行为等同于出现在必需参数的右侧。
在模棱两可的情况下,参数解析代码更倾向于参数左侧(在必需参数之前)。
可选组只能包含只认位置的参数。
可选组 仅限 用于过时代码。请勿在新的代码中使用可选组。
为了节省时间,尽量减少要学习的内容,实现第一次适用 Argument Clinic 的移植,上述练习简述的是“传统转换器”的用法。“传统转换器”只是一种简便用法,目的就是更容易地让现有代码移植为适用于 Argument Clinic 。说白了,在移植 Python 3.4 的代码时,可以考虑采用。
不过从长远来看,可能希望所有代码块都采用真正的 Argument Clinic 转换器语法。原因如下:
合适的转换器可读性更好,意图也更清晰。
有些格式单元是“传统转换器”无法支持的,因为这些格式需要带上参数,而传统转换器的语法不支持指定参数。
后续可能会有新版的参数解析库,提供超过 PyArg_ParseTuple() 支持的功能;而这种灵活性将无法适用于传统转换器转换的参数。
因此,若是不介意多花一点精力,请使用正常的转换器,而不是传统转换器。
简而言之,Argument Clinic(非传统)转换器的语法看起来像是 Python 函数调用。但如果函数没有明确的参数(所有函数都取默认值),则可以省略括号。因此 bool
和 bool()
是完全相同的转换器。
Argument Clinic 转换器的所有参数都只认关键字。所有 Argument Clinic 转换器均可接受以下参数:
c_default
该参数在 C 语言中的默认值。具体来说,将是在“解析函数”中声明的变量的初始化器。用法参见 the section on default values 。定义为字符串。
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
仅用于
object
和self
转换器。指定用于声明变量的 C 类型。 默认值是"PyObject *"
。
zeroes
仅用于字符串。如果为 True,则允许在值中嵌入 NUL 字节(
'\\0'
)。字符串的长度将通过名为的参数传入,跟在字符串参数的后面。
_length
请注意,并不是所有参数的组合都能正常生效。通常这些参数是由相应的 PyArg_ParseTuple
格式单元 实现的,行为是固定的。比如目前不能不指定 bitwise=True
就去调用 unsigned_short
。虽然完全有理由认为这样可行,但这些语义并没有映射到任何现有的格式单元。所以 Argument Clinic 并不支持。(或者说,至少目前还不支持。)
下表列出了传统转换器与真正的 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
转换器(或者 '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 预处理时进行硬编码。这个限制是故意设置的;以便简化对这种格式单元的支持,并允许以后进行优化。这个限制似乎并不合理;CPython 本身总是为 e
开头的格式单位参数传入静态的硬编码字符串。
参数的默认值可以是多个值中的一个。最简单的可以是字符串、int 或 float 字面量。
foo: str = "abc"
bar: int = 123
bat: float = 45.6
还可以使用 Python 的任何内置常量。
yep: bool = True
nope: bool = False
nada: object = None
对默认值 NULL
和简单表达式还提供特别的支持,下面将一一介绍。
NULL
对于字符串和对象参数而言,可以设为 None
,表示没有默认值。但这意味着会将 C 变量初始化为 Py_None
。为了方便起见,提供了一个特殊值``NULL``,目的就是为了让 Python 认为默认值就是 None
,而 C 变量则会初始化为 NULL
。
参数的默认值不仅可以是字面量。还可以是一个完整的表达式,可采用数学运算符及对象的属性。但这种支持并没有那么简单,因为存在一些不明显的语义。
请考虑以下例子:
foo: Py_ssize_t = sys.maxsize - 1
sys.maxsize
在不同的系统平台可能有不同的值。因此,Argument Clinic 不能简单地在本底环境对表达式求值并用 C 语言硬编码。所以默认值将用表达式的方式存储下来,运行的时候在请求函数签名时会被求值。
在对表达式进行求值时,可以使用什么命名空间呢?求值过程运行于内置模块的上下文中。 因此,如果模块带有名为 max_widgets
的属性,直接引用即可。
foo: Py_ssize_t = max_widgets
如果表达式不在当前模块中,就会去 sys.modules
查找。比如 sys.maxsize
就是如此找到的。(因为事先不知道用户会加载哪些模块到解释器中,所以最好只用到 Python 会预加载的模块。)
仅当运行时才对缺省值求值,意味着 Argument Clinic 无法计算出正确的 C 缺省值。所以需显式给出。在使用表达式时,必须同时用转换器的``c_default`` 参数指定 C 语言中的等价表达式。
foo: Py_ssize_t(c_default="PY_SSIZE_T_MAX - 1") = sys.maxsize - 1
还有一个问题也比较复杂。Argument Clinic 无法事先知道表达式是否有效。 解析只能保证看起来是有效值,但无法 实际 知晓。在用表达式时须十分小心,确保在运行时能得到有效值。
最后一点,由于表达式必须能表示为静态的 C 语言值,所以存在许多限制。 以下列出了不得使用的 Python 特性:
功能
行内 if 语句(3 if foo else 5
)
序列类自动解包(*[1, 2, 3]
)
列表、集合、字典的解析和生成器表达式。
元组、列表、集合、字典的字面量
Argument Clinic 生成的植入函数默认会返回 PyObject *
。但是通常 C 函数的任务是要对某些 C 类型进行计算,然后将其转换为 PyObject *
作为结果。Argument Clinic 可以将输入参数由 Python 类型转换为本地 C 类型——为什么不让它将返回值由本地 C 类型转换为 Python 类型呢?
这就是“返回值转换器”的用途。它将植入函数修改成返回某种 C 语言类型,然后在生成的(非植入)函数中添加代码,以便将 C 语言值转换为合适的 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
这些转换器都不需要参数。前3个转换器如果返回 -1 则表示出错。DecodeFSDefault
的返回值类型是 const char *
;若返回 NULL
指针则表示出错。
(还有一个 NoneType
转换器是实验性质的,成功时返回 Py_None
,失败则返回 NULL
,且不会增加 Py_None
的引用计数。此转换器是否值得适用,尚不明确)。
只要运行 Tools/clinic/clinic.py --converters
,即可查看 Argument Clinic 支持的所有返回值转换器,包括其参数。
如果已有一些函数比较相似,或许可以采用 Clinic 的“克隆”功能。 克隆之后能够复用以下内容:
参数,包括:
名称
转换器(带有全部参数)
默认值
参数前的文档字符串
类别 (只认位置、位置或关键字、只认关键字)
返回值转换器
唯一不从原函数中复制的是文档字符串;这样就能指定一个新的文档串。
下面是函数的克隆方法:
/*[clinic input]
module.class.new_function [as c_basename] = module.class.existing_function
Docstring for new_function goes here.
[clinic start generated code]*/
(原函数可以位于不同的模块或类中。示例中的 module.class
只是为了说明,两个 函数都必须使用全路径)。
Sorry, there’s no syntax for partially cloning a function, or cloning a function then modifying it. Cloning is an all-or nothing proposition.
另外,要克隆的函数必须在当前文件中已有定义。
下面的高级内容需要编写 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”参数。自动将 self 参数的 type
设为声明类型时指定的“指向实例的指针”。不过 Argument Clinic 的转换器可被覆盖,也即自己指定一个转换器。只要将自己的 self
参数作为块的第一个参数即可,并确保其转换器是 self_converter
的实例或其子类。
这有什么用呢?可用于覆盖 self
的类型,或为其给个不同的默认名称。
如何指定 self
对应的自定义类型呢?如果只有 self
类型相同的一两个函数,可以直接使用 Argument Clinic 现有的 self
转换器,把要用的类型作为 type
参数传入:
/*[clinic input]
_pickle.Pickler.dump
self: self(type="PicklerObject *")
obj: object
/
Write a pickled representation of the given object to the open file.
[clinic start generated code]*/
如果有很多函数将使用同一类型的 self
,则最好创建自己的转换器,继承自 self_converter
类但要覆盖其 type
成员:
/*[python input]
class PicklerObject_converter(self_converter):
type = "PicklerObject *"
[python start generated code]*/
/*[clinic input]
_pickle.Pickler.dump
self: PicklerObject
obj: object
/
Write a pickled representation of the given object to the open file.
[clinic start generated code]*/
Argument Clinic 为访问方法定义所在的类提供了便利。因为 heap type 方法需要获取模块级的运行状态,所以就十分有用。PyType_FromModuleAndSpec() 会将堆类型与模块关联起来。然后类就可用 PyType_GetModuleState() 获取模块状态了,比如利用模块的方法进行获取。
示例来自 Modules/zlibmodule.c
。首先,在 clinic 的输入块添加 defining_class
:
/*[clinic input]
zlib.Compress.compress
cls: defining_class
data: Py_buffer
Binary data to be compressed.
/
运行 Argument Clinic 工具后,会生成以下函数签名:
/*[clinic start generated code]*/
static PyObject *
zlib_Compress_compress_impl(compobject *self, PyTypeObject *cls,
Py_buffer *data)
/*[clinic end generated code: output=6731b3f0ff357ca6 input=04d00f65ab01d260]*/
现在,以下代码可以用 PyType_GetModuleState(cls)
获取模块状态了:
zlibstate *state = PyType_GetModuleState(cls);
每个方法只能有一个参数用到转换器,且须位于 self
之后 ,若未用到 self
则为第一个参数。该参数的类型为 PyTypeObject *
。__text_signature__
中不会包含该参数。
defining_class
转换器与 __init__
及 __new__
方法不兼容,他们不能使用 METH_METHOD
。
It is not possible to use defining_class
with slot methods. In order to fetch the module state from such methods, use PyType_GetModuleByDef() to look up the module and then PyModule_GetState() to fetch the module state. Example from the setattro
slot method in 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`` 的 Python 类。假如有个参数采用了 O&
格式,对此参数进行解析就会去调用某个“转换器函数” PyArg_ParseTuple() ,也就会用到自定义转换器。
自定义转换器类应命名为 *something*_converter
。只要按此规则命名,自定义转换器类就会在 Argument Clinic 中自动注册;转换器的名称就是去除了 _converter
后缀的类名。(通过元类完成)。
不得由 CConverter.__init__
派生子类。而应编写一个 converter_init()
函数。converter_init()
必须能接受一个 self
参数;所有后续的其他参数 必须 是只认关键字的参数。传给 Argument Clinic 转换器的所有参数都会传入自定义 converter_init()
函数。
CConverter
的其他一些成员,可能需要在自定义子类中定义。下面列出了目前的成员:
type
变量要采用的 C 语言数据类型。type
应为 int
之类的 Python 字符串,用于指定变量的类型。若为指针类型,则字符串应以 ' *'
结尾。
default
该参数的缺省值,为 Python 数据类型。若无缺省值,则为 unspecified
。
分享文章:创新互联Python教程:ArgumentClinic的用法
标题路径:http://www.shufengxianlan.com/qtweb/news25/313175.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联