Coding妙妙屋

软件漫谈:从底层实现到架构设计

0%

Python-Argument-Clinic

Python Argument Clinic功能解析

一、Argument Clinic概述

在阅读cpython源码的过程中,常常能在模块上看到如下的注释语句:

1
2
3
4
5
6
7
8
/*[clinic input]
module _pickle
class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
class _pickle.PicklerMemoProxy "PicklerMemoProxyObject *" "&PicklerMemoProxyType"
class _pickle.Unpickler "UnpicklerObject *" "&Unpickler_Type"
class _pickle.UnpicklerMemoProxy "UnpicklerMemoProxyObject *" "&UnpicklerMemoProxyType"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=4b3e113468a58e6c]*/

clinic是什么?本质是其实就是一个python脚本。

clinic有什么功能?自动生成cpython中参数解析功能的相关代码。

clinic是cpython中c文件的预处理器,可以通过固定格式的模板为builtins模块自动生成参数解析代码。如果自己维护cpython的参数解析代码,是一项较为繁琐的工作,需要在大量的地方维护冗余信息。当使用Argument Clinic功能时,我们不再需要自己进行参数解析,基于Argument Clinic生成的参数解析代码可以作为一个黑盒使用。

当前cpython中大部分参数解析的函数都使用了Argument Clinic模板自动生成功能,本文主要为所有打算编写自定义模块维护现有builtins 的同学提供基础指导。

二、基本语法

2.1 clinic input和output

clinic可以扫描文件中的指定行作为关键字,clinic input行之间的所有内容作为Clinic的模板输入,通常被称为Clinic block,也是我们需要重点关注和修改的部分。

  • clinic input start/*[clinic input]
  • clinic input end: [clinic start generated code]*/

构建python后执行命令行:./python .\Tools\clinic\clinic.py foo.c

可以扫描foo.c文件中的所有clinic block并生成代码,并在最后加上/*[clinic end generated code: output=xx input=xx]*/作为校验行,用于验证输入输出的对应关系。下面给出一个简单的例子:

1
2
3
4
5
6
7
8
/* foo.c */
/*[clinic input]

... clinic input

[clinic start generated code]*/
... clinic output
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=bb0565424c99751c]*/

2.2 创建clinic模板

本节使用python3.10的_pickle.c模块作为样例,解析clinic的模板格式。

  1. 首先需要在类顶部声明模块/类定义,类似于C语言常常在文件顶部进行声明。此处应对所有模块与类进行声明,其名称应该与Python界面的名称保持一致,可以使用PyModuleDefPyTypeObject中定义的名称。

    _pickle中模块及类的clinic定义样例:

    1
    2
    3
    4
    /*[clinic input]
    module _pickle
    class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
    [clinic start generated code]*/
  2. 创建函数块的clinic,应该由几部分组成:

    • 模块.类.方法名称(与python保持一致),(可选) 使用->在method后添加返回值类型;

    • 空行后写入参数名称及类型,每个参数都应占独立一行;

    • (可选) 为参数设置默认值,格式为name_of_parameter: converter = default_value

      converter是什么?

      我们需要了解参数应该被转换成什么类型,通常使用单个字符来表示某个特定类型,例如’O’表示对象,'s’表示字符串,'i’表示int型参数;详细可以参考:arg-parsing

    • (可选) 新增一行后缩进,为每个参数添加文档说明;

    • (可选) 若使用PyArg_ParseTuple() 解析参数,则所有参数都是位置相关,在最后加上/标记即可。如果需要使用关键字去解析参数PyArg_ParseTupleAndKeywords() 则不需要加/

    • (可选) 空行后,支持写入方法说明文档;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /*[clinic input]

    _pickle.Pickler.dump

    obj: 'O'
    argument document(optional)
    /

    Write a pickled representation of the given object to the open file.
    [clinic start generated code]*/

2.3 clinc代码生成

这里给出一个_pickle模块的例子验证代码生成的功能,我们构建python后,执行命令行:

./python .\Tools\clinic\clinic.py .\Modules\_pickle.c

可以看到clinic其实就是一个生成代码的python脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 未使用clinic时的原始pickler_dump函数
static PyObject *
Pickler_dump(PicklerObject *self, PyObject *args)
{
// 定义了一个临时变量用于PyObject类型的参数解析
PyObject *obj;

/* 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;
}

// 使用PyArg_ParseTuple进行参数解析
if (!PyArg_ParseTuple(args, "O:dump", &obj))
return NULL;

if (_Pickler_ClearBuffer(self) < 0)
return NULL;

if (dump(self, obj) < 0)
return NULL;

if (_Pickler_FlushToFile(self) < 0)
return NULL;
Py_RETURN_NONE;
}

使用clinic处理后发生了几点变化:

  • 方法名根据clinic中定义的发生了改变,按模块/类/方法的格式进行定义_pickle_Pickler_dump;
  • 除了固定的self参数,其余参数根据clinic定义自动生成,此处自动生成了PyObject *obj
  • 参数不再需要传递PyObject *args,再使用PyArg_ParseTuple一个一个解析,解析的过程将自动完成;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*[clinic input]

_pickle.Pickler.dump

obj: 'O'
/

Write a pickled representation of the given object to the open file.
[clinic start generated code]*/

static PyObject *
_pickle_Pickler_dump(PicklerObject *self, PyObject *obj)
/*[clinic end generated code: output=87ecad1261e02ac7 input=199cc5a0e7561167]*/
{
/* 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) {
PickleState *st = _Pickle_GetGlobalState();
PyErr_Format(st->PicklingError,
"Pickler.__init__() was not called by %s.__init__()",
Py_TYPE(self)->tp_name);
return NULL;
}

if (_Pickler_ClearBuffer(self) < 0)
return NULL;

if (dump(self, obj) < 0)
return NULL;

if (_Pickler_FlushToFile(self) < 0)
return NULL;

Py_RETURN_NONE;
}

// _pickle.h 头文件对应的宏定义也会自动生成以下代码块
PyDoc_STRVAR(_pickle_Pickler_dump__doc__,
"dump($self, obj, /)\n"
"--\n"
"\n"
"Write a pickled representation of the given object to the open file.");

#define _PICKLE_PICKLER_DUMP_METHODDEF \
{"dump", (PyCFunction)_pickle_Pickler_dump, METH_O, _pickle_Pickler_dump__doc__},

// 直接添加到PyMethodDef中即可使用
static struct PyMethodDef Pickler_methods[] = {
_PICKLE_PICKLER_DUMP_METHODDEF
_PICKLE_PICKLER_CLEAR_MEMO_METHODDEF
_PICKLE_PICKLER___SIZEOF___METHODDEF
{NULL, NULL} /* sentinel */
};

我们再添加一个int型参数来查看变化,可以看到:

  • 函数增加了一个int型参数,这和我们预想的一样;
  • 函数名变为_pickle_Pickler_dump_impl,clinic会在函数名后增加_impl后缀;
  • _pickle.h文件中新增了_pickle_Pickler_dump自动补全了参数解析的代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*[clinic input]

_pickle.Pickler.dump

obj: object
num: 'i'
/

Write a pickled representation of the given object to the open file.
[clinic start generated code]*/

static PyObject *
_pickle_Pickler_dump_impl(PicklerObject *self, PyObject *obj, int num)
/*[clinic end generated code: output=e80bca9c5c5a35a2 input=c6c713f75bc38e80]*/
{
...code block
}

/* _pickle.h文件,参数解析方式自动选择了METH_FASTCALL,对应_pickle_Pickler_dump(PicklerObject *self, PyObject *const *args, Py_ssize_t nargs)的回调函数参数类型
*/
#define _PICKLE_PICKLER_DUMP_METHODDEF \
{"dump", (PyCFunction)(void(*)(void))_pickle_Pickler_dump, METH_FASTCALL, _pickle_Pickler_dump__doc__},

static PyObject *
_pickle_Pickler_dump_impl(PicklerObject *self, PyObject *obj, int num);

/* 自动补充了参数解析的功能,我们写的函数逻辑作为底层被调用 */
static PyObject *
_pickle_Pickler_dump(PicklerObject *self, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *return_value = NULL;
PyObject *obj;
int num;

if (!_PyArg_CheckPositional("dump", nargs, 2, 2)) {
goto exit;
}
obj = args[0];
num = _PyLong_AsInt(args[1]);
if (num == -1 && PyErr_Occurred()) {
goto exit;
}
// 执行业务逻辑,不需要考虑参数解析
return_value = _pickle_Pickler_dump_impl(self, obj, num);

exit:
return return_value;
}

可以看到使用clinic后不管是多少参数,都可以自动生成参数解析代码块,用声明式编程取代传统的命令式编程,使得我们的编码过程更加简单。

三、参考文章

除此了基本的功能之外,clinic还提供了强大的高级特性用于通过模板生成各种类型的函数,详细可见如下文档:

Argument Clinic官方文档

参数解析API相关文档