1. 使用 C 或 C++ 扩展 Python¶
如果你会用 C,添加新的 Python 内置模块会很简单。以下两件不能用 Python 直接做的事,可以通过 extension modules 来实现:实现新的内置对象类型;调用 C 的库函数和系统调用。
为了支持扩展,Python API(应用程序编程接口)定义了一系列函数、宏和变量,可以访问 Python 运行时系统的大部分内容。Python 的 API 可以通过在一个 C 源文件中引用 "Python.h"
头文件来使用。
扩展模块的编写方式取决与你的目的以及系统设置;下面章节会详细介绍。
注解
C扩展接口特指CPython,扩展模块无法在其他Python实现上工作。在大多数情况下,应该避免写C扩展,来保持可移植性。举个例子,如果你的用例调用了C库或系统调用,你应该考虑使用 ctypes
模块或 cffi 库,而不是自己写C代码。这些模块允许你写Python代码来接口C代码,而且可移植性更好。不知为何编译失败了。
1.1. 一个简单的例子¶
让我们创建一个扩展模块 spam
(Monty Python 粉丝最喜欢的食物...) 并且想要创建对应 C 库函数 system()
1 的 Python 接口。 这个函数接受一个以 null 结尾的字符串参数并返回一个整数。 我们希望可以在 Python 中以如下方式调用此函数:
>>> import spam
>>> status = spam.system("ls -l")
首先创建一个 spammodule.c
文件。(传统上,如果一个模块叫 spam
,则对应实现它的 C 文件叫 spammodule.c
;如果这个模块名字非常长,比如 spammify
,则这个模块的文件可以直接叫 spammify.c
。)
文件中开始的两行是:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
这会导入 Python API(如果你喜欢,你可以在这里添加描述模块目标和版权信息的注释)。
注解
由于Python可能会定义一些影响某些系统上标准头文件的预处理器定义,因此在包含任何标准头文件之前,您*必须* include 这个文件:Python.h。
推荐总是在 Python.h
前定义 PY_SSIZE_T_CLEAN
。查看 提取扩展函数的参数 来了解这个宏的更多内容。
所有在 Python.h
中定义的用户可见的符号都具有 Py
或 PY
前缀,已在标准头文件中定义的那些除外。 考虑到便利性,也由于其在 Python 解释器中被广泛使用,"Python.h"
还包含了一些标准头文件: <stdio.h>
,<string.h>
,<errno.h>
和 <stdlib.h>
。 如果后面的头文件在你的系统上不存在,它还会直接声明函数 malloc()
,free()
和 realloc()
。
下面要做的事是将 C 函数添加到我们的扩展模块,当 Python 表达式 spam.system(string)
被求值时函数将被调用(我们很快就会看到它最终是如何被调用的):
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
return PyLong_FromLong(sts);
}
有个直接翻译参数列表的方法(例如单独的 “ls-l"
)到要传递给C函数的参数。C函数总是有两个参数,通常名字是 self 和 args 。
对模块级函数, self 参数指向模块对象;对于对象实例则指向方法。
args 参数是指向一个 Python 的 tuple 对象的指针,其中包含参数。 每个 tuple 项对应一个调用参数。 这些参数也全都是 Python 对象 --- 要在我们的 C 函数中使用它们就需要先将其转换为 C 值。 Python API 中的函数 PyArg_ParseTuple()
会检查参数类型并将其转换为 C 值。 它使用模板字符串确定需要的参数类型以及存储被转换的值的 C 变量类型。 细节将稍后说明。
PyArg_ParseTuple()
在所有参数都有正确类型且组成部分按顺序放在传递进来的地址里时,返回真(非零)。其在传入无效参数时返回假(零)。在后续例子里,还会抛出特定异常,使得调用的函数可以理解返回 NULL
(也就是例子里所见)。
1.2. 关于错误和异常¶
在 Python 解释器中有一个重要的惯例:当一个函数出错时,它应当设置异常条件并返回错误值(通常为 NULL
指针)。 异常存储于解释器内部的静态全局变量中;如此变量为 NULL
表示未发生异常。 还有第二个全局变量用于保存异常的“关联值”(即 raise
的第二个参数)。 第三个变量包含 Python 代码产生错误情况下的栈回溯信息。 这三个变量是 Python 中 sys.exc_info()
的结果在 C 中的对应物(请参阅 Python 库参考的 sys
模块部分)。 了解它们对于理解错误的传递方式是非常重要的。
Python API中定义了一些函数来设置这些变量。
最常用的就是 PyErr_SetString()
。 其参数是异常对象和 C 字符串。 异常对象一般是像 PyExc_ZeroDivisionError
这样的预定义对象。 C 字符串指明异常原因,并被转换为一个 Python 字符串对象存储为异常的“关联值”。
另一个有用的函数是 PyErr_SetFromErrno()
,仅接受一个异常对象,异常描述包含在全局变量 errno
中。最通用的函数还是 PyErr_SetObject()
,包含两个参数,分别为异常对象和异常描述。你不需要使用 Py_INCREF()
来增加传递到其他函数的参数对象的引用计数。
你可以通过 PyErr_Occurred()
在不造成破坏的情况下检测是否设置了异常。 这将返回当前异常对象,或者如果未发生异常则返回 NULL
。 你通常不需要调用 PyErr_Occurred()
来查看函数调用中是否发生了错误,因为你应该能从返回值中看出来。
当一个函数 f 调用另一个函数 g 时检测到后者出错了,f 应当自己返回一个错误值 (通常为 NULL
或 -1
)。 它 不应该 调用某个 PyErr_* 函数 --- 这类函数已经被 g 调用过了。 f 的调用者随后也应当返回一个错误来提示 它的 调用者,同样 不应该 调用 PyErr_*,依此类推 --- 错误的最详细原因已经由首先检测到它的函数报告了。 一旦这个错误到达 Python 解释器的主循环,它会中止当前执行的 Python 代码并尝试找出由 Python 程序员所指定的异常处理程序。
(在某些情况下,当模块确实能够通过调用其它 PyErr_* 函数给出更加详细的错误消息,并且在这些情况是可以这样做的。 但是按照一般规则,这是不必要的,并可能导致有关错误原因的信息丢失:大多数操作会由于种种原因而失败。)
想要忽略由一个失败的函数调用所设置的异常,异常条件必须通过调用 PyErr_Clear()
显式地被清除。 C 代码应当调用 PyErr_Clear()
的唯一情况是如果它不想将错误传给解释器而是想完全由自己来处理它(可能是尝试其他方法,或是假装没有出错)。
每次失败的 malloc()
调用必须转换为一个异常。 malloc()
(或 realloc()
)的直接调用者必须调用 PyErr_NoMemory()
来返回错误来提示。所有对象创建函数(例如 PyLong_FromLong()
)已经这么做了,所以这个提示仅用于直接调用 malloc()
的情况。
还要注意的是,除了 PyArg_ParseTuple()
等重要的例外,返回整数状态码的函数通常都是返回正值或零来表示成功,而以 -1
表示失败,如同 Unix 系统调用一样。
最后,当你返回一个错误指示器时要注意清理垃圾(通过为你已经创建的对象执行 Py_XDECREF()
或 Py_DECREF()
调用)!
选择引发哪个异常完全取决于你的喜好。 所有内置的 Python 异常都有对应的预声明 C 对象,例如 PyExc_ZeroDivisionError
,你可以直接使用它们。 当然,你应当明智地选择异常 --- 不要使用 PyExc_TypeError
来表示一个文件无法被打开 (那大概应该用 PyExc_IOError
)。 如果参数列表有问题,PyArg_ParseTuple()
函数通常会引发 PyExc_TypeError
。 如果你想要一个参数的值必须处于特定范围之内或必须满足其他条件,则适宜使用 PyExc_ValueError
。
你也可以为你的模块定义一个唯一的新异常。需要在文件前部声明一个静态对象变量,如:
static PyObject *SpamError;
并且在你的模块的初始化函数 (PyInit_spam()
) 中使用一个异常对象来初始化:
PyMODINIT_FUNC
PyInit_spam(void)
{
PyObject *m;
m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL;
SpamError = PyErr_NewException("spam.error", NULL, NULL);
Py_XINCREF(SpamError);
if (PyModule_AddObject(m, "error", SpamError) < 0) {
Py_XDECREF(SpamError);
Py_CLEAR(SpamError);
Py_DECREF(m);
return NULL;
}
return m;
}
注意异常对象的Python名字是 spam.error
。而 PyErr_NewException()
函数可以创建一个类,其基类为 Exception
(除非是另一个类传入以替换 NULL
), 细节参见 内置异常 。
同样注意的是创建类保存了 SpamError
的一个引用,这是有意的。为了防止被垃圾回收掉,否则 SpamError
随时会成为野指针。
一会讨论 PyMODINIT_FUNC
作为函数返回类型的用法。
spam.error
异常可以在扩展模块中抛出,通过 PyErr_SetString()
函数调用,如下:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
if (sts < 0) {
PyErr_SetString(SpamError, "System command failed");
return NULL;
}
return PyLong_FromLong(sts);
}
1.3. 回到例子¶
回到前面的例子,你应该明白下面的代码:
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
如果在参数列表中检测到错误,它将返回 NULL
(该值是返回对象指针的函数所使用的错误提示),这取决于 PyArg_ParseTuple()
设置的异常。 在其他情况下参数的字符串值会被拷贝到局部变量 command
。 这是一个指针赋值并且你不应该修改它所指向的字符串 (因此在标准 C 中,变量 command
应当被正确地声明为 const char *command
)。
下一个语句使用UNIX系统函数 system()
,传递给他的参数是刚才从 PyArg_ParseTuple()
取出的:
sts = system(command);
我们的 spam.system()
函数必须返回 sts
的值作为Python对象。这通过使用函数 PyLong_FromLong()
来实现。
return PyLong_FromLong(sts);
在这种情况下,会返回一个整数对象,(这个对象会在Python堆里面管理)。
如果你的C函数没有有用的返回值(返回 void 的函数),则必须返回 None
。(你可以用 Py_RETUN_NONE
宏来完成):
Py_INCREF(Py_None);
return Py_None;
Py_None
是特殊 Python 对象 None
所对应的 C 名称。 它是一个真正的 Python 对象而不是 NULL
指针,如我们所见,后者在大多数上下文中都意味着“错误”。
1.4. 模块方法表和初始化函数¶
为了展示 spam_system()
如何被Python程序调用。把函数声明为可以被Python调用,需要先定义一个方法表 "method table" 。
static PyMethodDef SpamMethods[] = {
...
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
...
{NULL, NULL, 0, NULL} /* Sentinel */
};
注意第三个参数 ( METH_VARARGS
) ,这个标志指定会使用C的调用惯例。可选值有 METH_VARARGS
、 METH_VARARGS | METH_KEYWORDS
。值 0
代表使用 PyArg_ParseTuple()
的陈旧变量。
如果单独使用 METH_VARARGS
,函数会等待Python传来tuple格式的参数,并最终使用 PyArg_ParseTuple()
进行解析。
METH_KEYWORDS
值表示接受关键字参数。这种情况下C函数需要接受第三个 PyObject *
对象,表示字典参数,使用 PyArg_ParseTupleAndKeywords()
来解析出参数。
这个方法表必须被模块定义结构所引用。
static struct PyModuleDef spammodule = {
PyModuleDef_HEAD_INIT,
"spam", /* name of module */
spam_doc, /* module documentation, may be NULL */
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
SpamMethods
};
这个结构体必须传递给解释器的模块初始化函数。初始化函数必须命名为 PyInit_name()
,其中 name 是模块的名字,并应该定义为非 static
,且在模块文件里:
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModule_Create(&spammodule);
}
注意 PyMODINIT_FUNC 声明了函数作为 PyObject *
返回类型,声明任何平台的链接生命,以及给C++生命函数的 extern "C"
。
当 Python 程序首次导入 spam
模块时, PyInit_spam()
会被调用。 (有关嵌入 Python 的注释参见下文。) 它将调用 PyModule_Create()
,该函数会返回一个模块对象,并基于在模块定义中找到的表将内置函数对象插入到新创建的模块中(该表是一个 PyMethodDef
结构体的数组)。 PyModule_Create()
返回一个指向它所创建的模块对象的指针。 它可能会因程度严重的特定错误而中止,或者在模块无法成功初始化时返回 NULL
。 初始化函数必须返回模块对象给其调用者,这样它就可以被插入到 sys.modules
中。
当嵌入Python时, PyInit_spam()
函数不会被自动调用,除非放在 PyImport_Inittab
表里。要添加模块到初始化表,使用 PyImport_AppendInittab()
,可选的跟着一个模块的导入。
int
main(int argc, char *argv[])
{
wchar_t *program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
exit(1);
}
/* Add a built-in module, before Py_Initialize */
if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
fprintf(stderr, "Error: could not extend in-built modules table\n");
exit(1);
}
/* Pass argv[0] to the Python interpreter */
Py_SetProgramName(program);
/* Initialize the Python interpreter. Required.
If this step fails, it will be a fatal error. */
Py_Initialize();
/* Optionally import the module; alternatively,
import can be deferred until the embedded script
imports it. */
pmodule = PyImport_ImportModule("spam");
if (!pmodule) {
PyErr_Print();
fprintf(stderr, "Error: could not import module 'spam'\n");
}
...
PyMem_RawFree(program);
return 0;
}
注解
要从 sys.modules
删除实体或导入已编译模块到一个进程里的多个解释器(或使用 fork()
而没用 exec()
)会在一些扩展模块上产生错误。扩展模块作者可以在初始化内部数据结构时给出警告。
更多关于模块的现实的例子包含在Python源码包的 Modules/xxmodule.c
中。这些文件可以用作你的代码模板,或者学习。脚本 modulator.py 包含在源码发行版或Windows安装中,提供了一个简单的GUI,用来声明需要实现的函数和对象,并且可以生成供填入的模板。脚本在 Tools/modulator/ 目录。查看README以了解用法。
注解
不像我们的 spam
例子, xxmodule
使用了 多阶段初始化 (Python3.5开始引入), PyInit_spam
会返回一个 PyModuleDef 结构体,然后创建的模块放到导入机制。细节参考 PEP 489 的多阶段初始化。
1.5. 编译和链接¶
在你能使用你的新写的扩展之前,你还需要做两件事情:使用 Python 系统来编译和链接。如果你使用动态加载,这取决于你使用的操作系统的动态加载机制;更多信息请参考编译扩展模块的章节( 构建C/C++扩展 章节),以及在 Windows 上编译需要的额外信息( 在 Windows 上构建 C 和 C++ 扩展 章节)。
如果你不使用动态加载,或者想要让模块永久性的作为Python解释器的一部分,就必须修改配置设置,并重新构建解释器。幸运的是在Unix上很简单,只需要把你的文件 ( spammodule.c
为例) 放在解压缩源码发行包的 Modules/
目录下,添加一行到 Modules/Setup.local
来描述你的文件:
spam spammodule.o
然后在顶层目录运行 make 来重新构建解释器。你也可以在 Modules/
子目录使用 make,但是你必须先重建 Makefile
文件,然后运行 'make Makefile' 命令。(你每次修改 Setup
文件都需要这样操作。)
如果你的模块需要额外的链接,这些内容可以列出在配置文件里,举个实例:
spam spammodule.o -lX11
1.6. 在C中调用Python函数¶
迄今为止,我们一直把注意力集中于让Python调用C函数,其实反过来也很有用,就是用C调用Python函数。这在回调函数中尤其有用。如果一个C接口使用回调,那么就要实现这个回调机制。
幸运的是,Python解释器是比较方便回调的,并给标准Python函数提供了标准接口。(这里就不再详述解析Python代码作为输入的方式,如果有兴趣可以参考 Python/pythonmain.c
中的 -c
命令代码。)
调用Python函数,首先Python程序要传递Python函数对象。应该提供个函数(或其他接口)来实现。当调用这个函数时,用全局变量保存Python函数对象的指针,还要调用 (Py_INCREF()
) 来增加引用计数,当然不用全局变量也没什么关系。例如如下:
static PyObject *my_callback = NULL;
static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
PyObject *result = NULL;
PyObject *temp;
if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
if (!PyCallable_Check(temp)) {
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
return NULL;
}
Py_XINCREF(temp); /* Add a reference to new callback */
Py_XDECREF(my_callback); /* Dispose of previous callback */
my_callback = temp; /* Remember new callback */
/* Boilerplate to return "None" */
Py_INCREF(Py_None);
result = Py_None;
}
return result;
}
这个函数必须使用 METH_VARARGS
标志注册到解释器,这在 模块方法表和初始化函数 章节会描述。 PyArg_ParseTuple()
函数及其参数的文档在 提取扩展函数的参数 。
Py_XINCREF()
和 Py_XDECREF()
这两个宏可增加/减少一个对象的引用计数,并且当存在 NULL
指针时仍可保证安全 (但请注意在这个上下文中 temp 将不为 NULL
)。 更多相关信息请参考 引用计数 章节。
随后,当要调用此函数时,你将调用 C 函数 PyObject_CallObject()
。 该函数有两个参数,它们都属于指针,指向任意 Python 对象:即 Python 函数,及其参数列表。 参数列表必须总是一个元组对象,其长度即参数的个数量。 要不带参数地调用 Python 函数,则传入 NULL
或一个空元组;要带一个参数调用它,则传入一个单元组。 Py_BuildValue()
会在其格式字符串包含一对圆括号内的零个或多个格式代码时返回一个元组。 例如:
int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
PyObject_CallObject()
返回Python对象指针,这也是Python函数的返回值。 PyObject_CallObject()
是一个对其参数 "引用计数无关" 的函数。例子中新的元组创建用于参数列表,并且在 PyObject_CallObject()
之后立即使用了 Py_DECREF()
。
PyEval_CallObject()
的返回值总是“新”的:要么是一个新建的对象;要么是已有对象,但增加了引用计数。所以除非你想把结果保存在全局变量中,你需要对这个值使用 Py_DECREF()
,即使你对里面的内容(特别!)不感兴趣。
但是在你这么做之前,很重要的一点是检查返回值不是 NULL
。 如果是的话,Python 函数会终止并引发异常。 如果调用 PyObject_CallObject()
的 C 代码是在 Python 中发起调用的,它应当立即返回一个错误来告知其 Python 调用者,以便解释器能打印栈回溯信息,或者让调用方 Python 代码能处理该异常。 如果这无法做到或不合本意,则应当通过调用 PyErr_Clear()
来清除异常。 例如:
if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);
依赖于具体的回调函数,你还要提供一个参数列表到 PyEval_CallObject()
。在某些情况下参数列表是由Python程序提供的,通过接口再传到回调函数。这样就可以不改变形式直接传递。另外一些时候你要构造一个新的tuple来传递参数。最简单的方法就是 Py_BuildValue()
函数构造tuple。例如,你要传递一个事件对象时可以用:
PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
注意 Py_DECREF(arglist)
所在处会立即调用,在错误检查之前。当然还要注意一些常规的错误,比如 Py_BuildValue()
可能会遭遇内存不足等等。
你还需要注意,用关键字参数调用 PyObject_Call()
,需要支持普通参数和关键字参数。有如如上例子中,我们使用 Py_BuildValue()
来构造字典。
PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
1.7. 提取扩展函数的参数¶
函数 PyArg_ParseTuple()
的声明如下:
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);
参数 arg 必须是一个元组对象,包含从 Python 传递给 C 函数的参数列表。format 参数必须是一个格式字符串,语法请参考 Python C/API 手册中的 语句解释及变量编译。剩余参数是各个变量的地址,类型要与格式字符串对应。
注意 PyArg_ParseTuple()
会检测他需要的Python参数类型,却无法检测传递给他的C变量地址,如果这里出错了,可能会在内存中随机写入东西,小心。
注意任何由调用者提供的Python对象引用是 借来的 引用;不要递减它们的引用计数!
一些调用的例子:
#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;
ok = PyArg_ParseTuple(args, ""); /* No arguments */
/* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
/* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
/* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
/* A pair of ints and a string, whose size is also returned */
/* Possible Python call: f((1, 2), 'three') */
{
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
f('spam')
f('spam', 'w')
f('spam', 'wb', 100000) */
}
{
int left, top, right, bottom, h, v;
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
&left, &top, &right, &bottom, &h, &v);
/* A rectangle and a point */
/* Possible Python call:
f(((0, 0), (400, 300)), (10, 10)) */
}
{
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */
}
1.8. 给扩展函数的关键字参数¶
函数 PyArg_ParseTupleAndKeywords()
声明如下:
int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
const char *format, char *kwlist[], ...);
arg 与 format 形参与 PyArg_ParseTuple()
函数所定义的一致。 kwdict 形参是作为第三个参数从 Python 运行时接收的关键字字典。 kwlist 形参是以 NULL
结尾的字符串列表,它被用来标识形参;名称从左至右与来自 format 的类型信息相匹配。 如果执行成功,PyArg_ParseTupleAndKeywords()
会返回真值,否则返回假值并引发一个适当的异常。
注解
嵌套的元组在使用关键字参数时无法生效,不在 kwlist 中的关键字参数会导致 TypeError
异常。
如下是使用关键字参数的例子模块,作者是 Geoff Philbrick (phibrick@hks.com):
#define PY_SSIZE_T_CLEAN /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
int voltage;
const char *state = "a stiff";
const char *action = "voom";
const char *type = "Norwegian Blue";
static char *kwlist[] = {"voltage", "state", "action", "type", NULL};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
&voltage, &state, &action, &type))
return NULL;
printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
action, voltage);
printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);
Py_RETURN_NONE;
}
static PyMethodDef keywdarg_methods[] = {
/* The cast of the function is necessary since PyCFunction values
* only take two PyObject* parameters, and keywdarg_parrot() takes
* three.
*/
{"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
"Print a lovely skit to standard output."},
{NULL, NULL, 0, NULL} /* sentinel */
};
static struct PyModuleDef keywdargmodule = {
PyModuleDef_HEAD_INIT,
"keywdarg",
NULL,
-1,
keywdarg_methods
};
PyMODINIT_FUNC
PyInit_keywdarg(void)
{
return PyModule_Create(&keywdargmodule);
}
1.9. 构造任意值¶
这个函数与 PyArg_ParseTuple()
很相似,声明如下:
PyObject *Py_BuildValue(const char *format, ...);
接受一个格式字符串,与 PyArg_ParseTuple()
相同,但是参数必须是原变量的地址指针(输入给函数,而非输出)。最终返回一个Python对象适合于返回C函数调用给Python代码。
一个与 PyArg_ParseTuple()
的不同是,后面可能需要的要求返回一个元组(Python参数里诶包总是在内部描述为元组),比如用于传递给其他Python函数以参数。 Py_BuildValue()
并不总是生成元组,在多于1个参数时会生成元组,而如果没有参数则返回 None
,一个参数则直接返回该参数的对象。如果要求强制生成一个长度为空的元组,或包含一个元素的元组,需要在格式字符串中加上括号。
例子(左侧是调用,右侧是Python值结果):
Py_BuildValue("") None
Py_BuildValue("i", 123) 123
Py_BuildValue("iii", 123, 456, 789) (123, 456, 789)
Py_BuildValue("s", "hello") 'hello'
Py_BuildValue("y", "hello") b'hello'
Py_BuildValue("ss", "hello", "world") ('hello', 'world')
Py_BuildValue("s#", "hello", 4) 'hell'
Py_BuildValue("y#", "hello", 4) b'hell'
Py_BuildValue("()") ()
Py_BuildValue("(i)", 123) (123,)
Py_BuildValue("(ii)", 123, 456) (123, 456)
Py_BuildValue("(i,i)", 123, 456) (123, 456)
Py_BuildValue("[i,i]", 123, 456) [123, 456]
Py_BuildValue("{s:i,s:i}",
"abc", 123, "def", 456) {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))
1.10. 引用计数¶
在C/C++语言中,程序员负责动态分配和回收堆(heap)当中的内存。在C里,通过函数 malloc()
和 free()
来完成。在C++里是操作 new
和 delete
来实现相同的功能。
每个由 malloc()
分配的内存块,最终都要由 free()
退回到可用内存池里面去。而调用 free()
的时机非常重要,如果一个内存块忘了 free()
则会导致内存泄漏,这块内存在程序结束前将无法重新使用。这叫做 内存泄漏 。而如果对同一内存块 free()
了以后,另外一个指针再次访问,则再次使用 malloc()
复用这块内存会导致冲突。这叫做 野指针 。等同于使用未初始化的数据,core dump,错误结果,神秘的崩溃等。
内存泄露往往发生在一些并不常见的代码流程上面。比如一个函数申请了内存以后,做了些计算,然后释放内存块。现在一些对函数的修改可能增加对计算的测试并检测错误条件,然后过早的从函数返回了。这很容易忘记在退出前释放内存,特别是后期修改的代码。这种内存泄漏,一旦引入,通常很长时间都难以检测到,错误退出被调用的频度较低,而现代电脑又有非常巨大的虚拟内存,所以泄漏仅在长期运行或频繁调用泄漏函数时才会变得明显。因此,有必要避免内存泄漏,通过代码规范会策略来最小化此类错误。
Python通过 malloc()
和 free()
包含大量的内存分配和释放,同样需要避免内存泄漏和野指针。他选择的方法就是 引用计数 。其原理比较简单:每个对象都包含一个计数器,计数器的增减与对象引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义了,对象就可以删除了。
另一个叫法是 自动垃圾回收 。(有时引用计数也被看作是垃圾回收策略,于是这里的"自动"用以区分两者)。自动垃圾回收的优点是用户不需要明确的调用 free()
。(另一个优点是改善速度或内存使用,然而这并不难)。缺点是对C,没有可移植的自动垃圾回收器,而引用计数则可以可移植的实现(只要 malloc()
和 free()
函数是可用的,这也是C标准担保的)。也许以后有一天会出现可移植的自动垃圾回收器,但在此前我们必须与引用计数一起工作。
Python使用传统的引用计数实现,也提供了循环监测器,用以检测引用循环。这使得应用无需担心直接或间接的创建了循环引用,这是引用计数垃圾收集的一个弱点。引用循环是对象(可能直接)的引用了本身,所以循环中的每个对象的引用计数都不是0。典型的引用计数实现无法回收处于引用循环中的对象,或者被循环所引用的对象,哪怕没有循环以外的引用了。
循环探测器可以检测垃圾循环并回收。 gc
模块提供了方法运行探测器 ( collect()
函数) ,而且可以在运行时配置禁用探测器。循环探测器被当作可选组件,默认是包含的,也可以在构建时禁用,在Unix平台(包括Mac OS X)使用 --without-cycle-gc
选项到 configure 脚本。如果循环探测器被禁用, gc
模块就不可用了。
1.10.1. Python中的引用计数¶
有两个宏 Py_INCREF(x)
和 Py_DECREF(x)
,会处理引用计数的增减。 Py_DECREF()
也会在引用计数到达0时释放对象。为了灵活,并不会直接调用 free()
,而是通过对象的 类型对象 的函数指针来调用。为了这个目的(或其他的),每个对象同时包含一个指向自身类型对象的指针。
最大的问题依旧:何时使用 Py_INCREF(x)
和 Py_DECREF(x)
?我们首先引入一些概念。没有人"拥有"一个对象,你可以 拥有一个引用 到一个对象。一个对象的引用计数定义为拥有引用的数量。引用的拥有者有责任调用 Py_DECREF()
,在引用不再需要时。引用的拥有关系可以被传递。有三种办法来处置拥有的引用:传递、存储、调用 Py_DECREF()
。忘记处置一个拥有的引用会导致内存泄漏。
还可以 借用 2 一个对象的引用。借用的引用不应该调用 Py_DECREF()
。借用者必须确保不能持有对象超过拥有者借出的时间。在拥有者处置对象后使用借用的引用是有风险的,应该完全避免 3 。
借用相对于引用的优点是你无需担心整条路径上代码的引用,或者说,通过借用你无需担心内存泄漏的风险。借用的缺点是一些看起来正确代码上的借用可能会在拥有者处置后使用对象。
借用可以变为拥有引用,通过调用 Py_INCREF()
。这不会影响已经借出的拥有者的状态。这回创建一个新的拥有引用,并给予完全的拥有者责任(新的拥有者必须恰当的处置引用,就像之前的拥有者那样)。
1.10.2. 拥有规则¶
当一个对象引用传递进出一个函数时,函数的接口应该指定拥有关系的传递是否包含引用。
大多数函数返回一个对象的引用,并传递引用拥有关系。通常,所有创建对象的函数,例如 PyLong_FromLong()
和 Py_BuildValue()
,会传递拥有关系给接收者。即便是对象不是真正新的,你仍然可以获得对象的新引用。一个实例是 PyLong_FromLong()
维护了一个流行值的缓存,并可以返回已缓存项目的新引用。
很多另一个对象提取对象的函数,也会传递引用关系,例如 PyObject_GetAttrString()
。这里的情况不够清晰,一些不太常用的例程是例外的 PyTuple_GetItem()
, PyList_GetItem()
, PyDict_GetItem()
, PyDict_GetItemString()
都是返回从元组、列表、字典里借用的引用。
函数 PyImport_AddModule()
也会返回借用的引用,哪怕可能会返回创建的对象:这个可能因为一个拥有的引用对象是存储在 sys.modules
里。
当你传递一个对象引用到另一个函数时,通常函数是借用出去的。如果需要存储,就使用 Py_INCREF()
来变成独立的拥有者。这个规则有两个重要的例外: PyTuple_SetItem()
和 PyList_SetItem()
。这些函数接受传递来的引用关系,哪怕会失败!(注意 PyDict_SetItem()
及其同类不会接受引用关系,他们是"正常的")。
当一个C函数被Python调用时,会从调用方传来的参数借用引用。调用者拥有对象的引用,所以借用的引用生命周期可以保证到函数返回。只要当借用的引用需要存储或传递时,就必须转换为拥有的引用,通过调用 Py_INCREF()
。
Python调用从C函数返回的对象引用时必须是拥有的引用---拥有关系被从函数传递给调用者。
1.10.3. 危险的薄冰¶
有少数情况下,借用的引用看起来无害,但却可能导致问题。这通常是因为解释器的隐式调用,并可能导致引用拥有者处置这个引用。
首先需要特别注意的情况是使用 Py_DECREF()
到一个无关对象,而这个对象的引用是借用自一个列表的元素。举个实例:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}
这个函数首先借用一个引用 list[0]
,然后替换 list[1]
为值 0
,最后打印借用的引用。看起来无害是吧,但却不是。
我们跟着控制流进入 PyList_SetItem()
。列表拥有者引用了其所有成员,所以当成员1被替换时,就必须处置原来的成员1。现在假设原来的成员1是用户定义类的实例,且假设这个类定义了 __del__()
方法。如果这个类实例的引用计数是1,那么处置动作就会调用 __del__()
方法。
既然是Python写的, __del__()
方法可以执行任意Python代码。是否可能在 bug()
的 item
废止引用呢,是的。假设列表传递到 bug()
会被 __del__()
方法所访问,就可以执行一个语句来实现 del list[0]
,然后假设这是最后一个对对象的引用,就需要释放内存,从而使得 item
无效化。
解决方法是,当你知道了问题的根源,就容易了:临时增加引用计数。正确版本的函数代码如下:
void
no_bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_INCREF(item);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0);
Py_DECREF(item);
}
这是个真实的故事。一个旧版本的Python包含了这个bug的变种,而一些人花费了大量时间在C调试器上去寻找为什么 __del__()
方法会失败。
这个问题的第二种情况是借用的引用涉及线程的变种。通常,Python解释器里多个线程无法进入对方的路径,因为有个全局锁保护着Python整个对象空间。但可以使用宏 Py_BEGIN_ALLOW_THREADS
来临时释放这个锁,重新获取锁用 Py_END_ALLOW_THREADS
。这通常围绕在阻塞I/O调用外,使得其他线程可以在等待I/O期间使用处理器。显然,如下函数会跟之前那个有一样的问题:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_BEGIN_ALLOW_THREADS
...some blocking I/O call...
Py_END_ALLOW_THREADS
PyObject_Print(item, stdout, 0); /* BUG! */
}
1.10.4. NULL指针¶
通常,接受对象引用作为参数的函数不希望你传给它们 NULL
指针,并且当你这样做时将会转储核心(或在以后导致核心转储)。 返回对象引用的函数通常只在要指明发生了异常时才返回 NULL
。 不检测 NULL
参数的原因在于这些函数经常要将它们所接收的对象传给其他函数 --- 如果每个函数都检测 NULL
,将会导致大量的冗余检测而使代码运行得更缓慢。
更好的做法是仅在“源头”上检测 NULL
,即在接收到一个可能为 NULL
的指针,例如来自 malloc()
或是一个可能引发异常的函数的时候。
Py_INCREF()
和 Py_DECREF()
等宏不会检测 NULL
指针 --- 但是,它们的变种 Py_XINCREF()
和 Py_XDECREF()
则会检测。
用于检测特定对象类型的宏 (Pytype_Check()
) 不会检测 NULL
指针 --- 同样地,有大量代码会连续调用这些宏来测试一个对象是否为几种不同预期类型之一,这将会生成冗余的测试。 不存在带有 NULL
检测的变体。
C 函数调用机制会保证传给 C 函数的参数列表 (本示例中为 args
) 绝不会为 NULL
--- 实际上它会保证其总是为一个元组 4。
任何时候将 NULL
指针“泄露”给 Python 用户都会是个严重的错误。
1.11. 在C++中编写扩展¶
还可以在C++中编写扩展模块,只是有些限制。如果主程序(Python解释器)是使用C编译器来编译和链接的,全局或静态对象的构造器就不能使用。而如果是C++编译器来链接的就没有这个问题。函数会被Python解释器调用(通常就是模块初始化函数)必须声明为 extern "C"
。而是否在 extern "C" {...}
里包含Python头文件则不是那么重要,因为如果定义了符号 __cplusplus
则已经是这么声明的了(所有现代C++编译器都会定义这个符号)。
1.12. 给扩展模块提供C API¶
很多扩展模块提供了新的函数和类型供Python使用,但有时扩展模块里的代码也可以被其他扩展模块使用。例如,一个扩展模块可以实现一个类型 "collection" 看起来是没有顺序的。就像是Python列表类型,拥有C API允许扩展模块来创建和维护列表,这个新的集合类型可以有一堆C函数用于给其他扩展模块直接使用。
开始看起来很简单:只需要编写函数(无需声明为 static
),提供恰当的头文件,以及C API的文档。实际上在所有扩展模块都是静态链接到Python解释器时也是可以正常工作的。当模块以共享库链接时,一个模块中的符号定义对另一个模块不可见。可见的细节依赖于操作系统,一些系统的Python解释器使用全局命名空间(例如Windows),有些则在链接时需要一个严格的已导入符号列表(一个例子是AIX),或者提供可选的不同策略(如Unix系列)。即便是符号是全局可见的,你要调用的模块也可能尚未加载。
可移植性需要不能对符号可见性做任何假设。这意味着扩展模块里的所有符号都应该声明为 static
,除了模块的初始化函数,来避免与其他扩展模块的命名冲突(在段落 模块方法表和初始化函数 中讨论) 。这意味着符号应该 必须 通过其他导出方式来供其他扩展模块访问。
Python提供了一个特别的机制来传递C级别信息(指针),从一个扩展模块到另一个:Capsules。一个Capsule是一个Python数据类型,会保存指针( void* )。Capsule只能通过其C API来创建和访问,但可以像其他Python对象一样的传递。通常,我们可以指定一个扩展模块命名空间的名字。其他扩展模块可以导入这个模块,获取这个名字的值,然后从Capsule获取指针。
Capsule可以用多种方式导出C API给扩展模块。每个函数可以用自己的Capsule,或者所有C API指针可以存储在一个数组里,数组地址再发布给Capsule。存储和获取指针也可以用多种方式,供客户端模块使用。
无论你选择哪个方法,正确地为你的 Capsule 命名都很重要。 函数 PyCapsule_New()
接受一个名称形参 (const char*);允许你传入一个 NULL
作为名称,但我们强烈建议你指定一个名称。 正确地命名的 Capsule 提供了一定程序的运行时类型安全;没有可行的方式能区分两个未命名的 Capsule。
通常来说,Capsule用于暴露C API,其名字应该遵循如下规范:
modulename.attributename
便利函数 PyCapsule_Import()
可以方便的载入通过Capsule提供的C API,仅在Capsule的名字匹配时。这个行为为C API用户提供了高度的确定性来载入正确的C API。
如下例子展示了将大部分负担交由导出模块作者的方法,适用于常用的库模块。其会存储所有C API指针(例子里只有一个)在 void 指针的数组里,并使其值变为Capsule。对应的模块头文件提供了宏来管理导入模块和获取C API指针;客户端模块只需要在访问C API前调用这个宏即可。
导出的模块修改自 spam
模块,来自 一个简单的例子 段落。函数 spam.system()
不会直接调用C库函数 system()
,但一个函数 PySpam_System()
会负责调用,当然现实中会更复杂些(例如添加 "spam" 到每个命令)。函数 PySpam_System()
也会导出给其他扩展模块。
函数 PySpam_System()
是个纯C函数,声明 static
就像其他地方那样:
static int
PySpam_System(const char *command)
{
return system(command);
}
函数 spam_system()
按照如下方式修改:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = PySpam_System(command);
return PyLong_FromLong(sts);
}
在模块开头,在此行后:
#include <Python.h>
添加另外两行:
#define SPAM_MODULE
#include "spammodule.h"
#define
用于告知头文件需要包含给导出的模块,而不是客户端模块。最终,模块的初始化函数必须负责初始化C API指针数组:
PyMODINIT_FUNC
PyInit_spam(void)
{
PyObject *m;
static void *PySpam_API[PySpam_API_pointers];
PyObject *c_api_object;
m = PyModule_Create(&spammodule);
if (m == NULL)
return NULL;
/* Initialize the C API pointer array */
PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;
/* Create a Capsule containing the API pointer array's address */
c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);
if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) {
Py_XDECREF(c_api_object);
Py_DECREF(m);
return NULL;
}
return m;
}
注意 PySpam_API
声明为 static
;此外指针数组会在 PyInit_spam()
结束后消失!
头文件 spammodule.h
里的一堆工作,看起来如下所示:
#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Header file for spammodule */
/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)
/* Total number of C API pointers */
#define PySpam_API_pointers 1
#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */
static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;
#else
/* This section is used in modules that use spammodule's API */
static void **PySpam_API;
#define PySpam_System \
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])
/* Return -1 on error, 0 on success.
* PyCapsule_Import will set an exception if there's an error.
*/
static int
import_spam(void)
{
PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
return (PySpam_API != NULL) ? 0 : -1;
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(Py_SPAMMODULE_H) */
客户端模块必须在其初始化函数里按顺序调用函数 import_spam()
(或其他宏)才能访问函数 PySpam_System()
。
PyMODINIT_FUNC
PyInit_client(void)
{
PyObject *m;
m = PyModule_Create(&clientmodule);
if (m == NULL)
return NULL;
if (import_spam() < 0)
return NULL;
/* additional initialization can happen here */
return m;
}
这种方法的主要缺点是,文件 spammodule.h
过于复杂。当然,对每个要导出的函数,基本结构是相似的,所以只需要学习一次。
最后需要提醒的是Capsule提供了额外的功能,用于存储在Capsule里的指针的内存分配和释放。细节参考 Python/C API参考手册的章节 Capsule 对象 和Capsule的实现(在Python源码发行包的 Include/pycapsule.h
和 Objects/pycapsule.c
)。
脚注