Python 3 - 使用 C 的扩展编程

  • 简述

    您使用任何编译语言(如 C、C++ 或 Java)编写的任何代码都可以集成或导入到另一个 Python 脚本中。此代码被视为“扩展”。
    Python 扩展模块只不过是一个普通的 C 库。在 Unix 机器上,这些库通常以.so(对于共享对象)。在 Windows 机器上,您通常会看到.dll(对于动态链接库)。
  • 编写扩展的先决条件

    要开始编写您的扩展,您将需要 Python 头文件。
    • 在 Unix 机器上,这通常需要安装特定于开发人员的包,例如python2.5-dev
    • Windows 用户在使用二进制 Python 安装程序时将这些标头作为包的一部分获取。
    此外,假设您对 C 或 C++ 有很好的了解,可以使用 C 编程编写任何 Python 扩展。
  • 先看一个 Python Extension

    第一次查看 Python 扩展模块时,您需要将代码分为四个部分 -
    • 头文件Python.h
    • 您希望作为模块接口公开的 C 函数。
    • 一个映射函数名称的表,因为 Python 开发人员将它们视为扩展模块中的 C 函数。
    • 一个初始化函数。
  • 头文件 Python.h

    您需要在 C 源文件中包含Python.h头文件,这使您可以访问用于将模块挂接到解释器的内部 Python API。
    确保在您可能需要的任何其他标头之前包含 Python.h。您需要在包含要从 Python 调用的函数之后。
  • C 函数

    你的函数的 C 实现的签名总是采用以下三种形式之一 -
    
    static PyObject *MyFunction( PyObject *self, PyObject *args );
    static PyObject *MyFunctionWithKeywords(PyObject *self, PyObject *args, PyObject *kw);
    static PyObject *MyFunctionWithNoArgs( PyObject *self );
    
    前面的每一个声明都返回一个 Python 对象。Python 中没有像 C 中那样的void函数这样的东西。如果你不希望你的函数返回一个值,返回 Python 的 C 等价物None价值。Python 头文件定义了一个宏,Py_RETURN_NONE,它为我们做这件事。
    您的 C 函数的名称可以是您喜欢的任何名称,因为它们在扩展模块之外永远不会出现。它们被定义为静态函数。
    您的 C 函数通常通过将 Python 模块和函数名称组合在一起来命名,如下所示 -
    
    static PyObject *module_func(PyObject *self, PyObject *args) {
       /* Do your stuff here. */
       Py_RETURN_NONE;
    }
    
    这是模块module中名为func的 Python 函数。您将把指向 C 函数的指针放入通常在源代码中紧随其后的模块的方法表中。
  • 方法映射表

    此方法表是 PyMethodDef 结构的简单数组。该结构看起来像这样 -
    
    struct PyMethodDef {
       char *ml_name;
       PyCFunction ml_meth;
       int ml_flags;
       char *ml_doc;
    };
    
    这是该结构成员的描述 -
    • ml_name− 这是 Python 解释器在 Python 程序中使用时呈现的函数名称。
    • ml_meth− 这是具有上一节中描述的任何一个签名的函数的地址。
    • ml_flags− 这告诉解释器 ml_meth 使用的是三个签名中的哪一个。
      • 该标志的值通常为 METH_VARARGS。
      • 如果你想允许关键字参数进入你的函数,这个标志可以与 METH_KEYWORDS 按位或运算。
      • 这也可以有一个 METH_NOARGS 值,表示您不想接受任何参数。
    • ml_doc− 这是函数的文档字符串,如果你不想写的话,它可以是 NULL。
    该表需要用一个标记终止,该标记由适当成员的 NULL 和 0 值组成。

    例子

    对于上面定义的函数,我们有以下方法映射表 -
    
    static PyMethodDef module_methods[] = {
       { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
       { NULL, NULL, 0, NULL }
    };
    
  • 初始化函数

    扩展模块的最后一部分是初始化函数。加载模块时,Python 解释器会调用此函数。要求函数被命名initModule,其中Module是模块的名称。
    初始化函数需要从您将要构建的库中导出。Python 标头定义 PyMODINIT_FUNC 以包含适当的咒语,以便在我们正在编译的特定环境中发生这种情况。您所要做的就是在定义函数时使用它。
    您的 C 初始化函数通常具有以下总体结构 -
    
    PyMODINIT_FUNC initModule() {
       Py_InitModule3(func, module_methods, "docstring...");
    }
    
    这是对Py_InitModule3功能 -
    • func− 这是要导出的函数。
    • module_methods− 这是上面定义的映射表名称。
    • docstring− 这是您要在分机中给出的评论。
    把所有这些放在一起,它看起来像下面这样 -
    
    #include <Python.h>
    static PyObject *module_func(PyObject *self, PyObject *args) {
       /* Do your stuff here. */
       Py_RETURN_NONE;
    }
    static PyMethodDef module_methods[] = {
       { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
       { NULL, NULL, 0, NULL }
    };
    PyMODINIT_FUNC initModule() {
       Py_InitModule3(func, module_methods, "docstring...");
    }
    

    例子

    一个使用上述所有概念的简单示例 -
    
    #include <Python.h>
    static PyObject* helloworld(PyObject* self)
    {
       return Py_BuildValue("s", "Hello, Python extensions!!");
    }
    static char helloworld_docs[] =
       "helloworld( ): Any message you want to put here!!\n";
    static PyMethodDef helloworld_funcs[] = {
       {"helloworld", (PyCFunction)helloworld, 
       METH_NOARGS, helloworld_docs},
       {NULL}
    };
    void inithelloworld(void)
    {
       Py_InitModule3("helloworld", helloworld_funcs, "Extension module example!");
    }
    
    这里Py_BuildValue函数用于构建 Python 值。将以上代码保存在 hello.c 文件中。我们将看到如何编译和安装这个模块以从 Python 脚本中调用。
  • 构建和安装扩展

    distutils包使得以标准方式分发 Python 模块(包括纯 Python 和扩展模块)变得非常容易。模块以源代码形式分发,通过通常称为setup.py的安装脚本构建和安装。
    对于上述模块,您需要准备以下 setup.py 脚本 -
    
    from distutils.core import setup, Extension
    setup(name = 'helloworld', version = '1.0',  \
       ext_modules = [Extension('helloworld', ['hello.c'])])
    
    现在,使用以下命令执行所有需要的编译和链接步骤,使用正确的编译器和链接器命令和标志,并将生成的动态库复制到适当的目录中 -
    
    $ python setup.py install
    
    在基于 Unix 的系统上,您很可能需要以 root 身份运行此命令才能获得写入站点包目录的权限。这在 Windows 上通常不是问题。
  • 导入扩展

    安装扩展后,您将能够在 Python 脚本中导入和调用该扩展,如下所示 -

    例子

    
    #!/usr/bin/python3
    import helloworld
    print helloworld.helloworld()
    

    输出

    这将产生以下结果 -
    
    Hello, Python extensions!!
    
  • 传递函数参数

    由于您很可能希望定义接受参数的函数,因此您可以为 C 函数使用其他签名之一。例如,接受一些参数的以下函数将这样定义 -
    
    static PyObject *module_func(PyObject *self, PyObject *args) {
       /* Parse args and do something interesting here. */
       Py_RETURN_NONE;
    }
    
    包含新函数条目的方法表如下所示 -
    
    static PyMethodDef module_methods[] = {
       { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
       { "func", module_func, METH_VARARGS, NULL },
       { NULL, NULL, 0, NULL }
    };
    
    您可以使用 API PyArg_ParseTuple函数从传递给 C 函数的一个 PyObject 指针中提取参数。
    PyArg_ParseTuple 的第一个参数是 args 参数。这是您要解析的对象。第二个参数是一个格式字符串,描述了您希望它们出现的参数。每个参数由格式字符串中的一个或多个字符表示,如下所示。
    
    static PyObject *module_func(PyObject *self, PyObject *args) {
       int i;
       double d;
       char *s;
       if (!PyArg_ParseTuple(args, "ids", &i, &d, &s)) {
          return NULL;
       }
       
       /* Do something interesting here. */
       Py_RETURN_NONE;
    }
    

    输出

    编译模块的新版本并导入它使您能够使用任意数量的任意类型的参数调用新函数 -
    
    module.func(1, s = "three", d = 2.0)
    module.func(i = 1, d = 2.0, s = "three")
    module.func(s = "three", d = 2.0, i = 1)
    
    你可能会想出更多的变化。
  • PyArg_ParseTuple 函数

    这是标准签名PyArg_ParseTuple功能 -
    
    int PyArg_ParseTuple(PyObject* tuple,char* format,...)
    
    此函数返回 0 表示错误,返回不等于 0 的值表示成功。元组是 PyObject*,它是 C 函数的第二个参数。这里的format是一个 C 字符串,描述了强制和可选参数。
    这是格式代码的列表PyArg_ParseTuple功能 -
    代码 C型 意义
    c char 长度为 1 的 Python 字符串变成 C 字符。
    d double 一个 Python 浮点数变成了一个 C double。
    f float Python 浮点数变成了 C 浮点数。
    i int Python int 变成了 C int。
    l long Python int 变成了 C long。
    L long long Python int 变成 C long long
    O PyObject* 获取对 Python 参数的非 NULL 借用引用。
    s char* 没有嵌入空值的 Python 字符串到 C char*。
    s# char*+int 任何 Python 字符串到 C 地址和长度。
    t# char*+int 只读单段缓冲区到 C 地址和长度。
    u Py_UNICODE* 没有嵌入空值的 Python Unicode 到 C。
    u# Py_UNICODE*+int 任何 Python Unicode C 地址和长度。
    w# char*+int 将单段缓冲区读/写到 C 地址和长度。
    z char* 与 s 一样,也接受 None(将 C char* 设置为 NULL)。
    z# char*+int 与 s# 一样,也接受 None(将 C char* 设置为 NULL)。
    (...) as per ... Python 序列被视为每个项目一个参数。
    | 以下参数是可选的。
    : 格式结束,后跟错误消息的函数名称。
    ; 格式结束,后跟整个错误消息文本。
  • 返回值

    Py_BuildValue采用格式字符串,就像PyArg_ParseTuple一样。您不是传递正在构建的值的地址,而是传递实际值。这是一个显示如何实现添加功能的示例 -
    
    static PyObject *foo_add(PyObject *self, PyObject *args) {
       int a;
       int b;
       if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
          return NULL;
       }
       return Py_BuildValue("i", a + b);
    }
    
    如果用 Python 实现,这就是它的样子 -
    
    def add(a, b):
       return (a + b)
    
    您可以按如下方式从函数返回两个值。这将使用 Python 中的列表来捕获。
    
    static PyObject *foo_add_subtract(PyObject *self, PyObject *args) {
       int a;
       int b;
       if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
          return NULL;
       }
       return Py_BuildValue("ii", a + b, a - b);
    }
    
    如果用 Python 实现,这就是它的样子 -
    
    def add_subtract(a, b):
       return (a + b, a - b)
    
  • Py_BuildValue函数_

    这是标准签名Py_BuildValue功能 -
    
    PyObject* Py_BuildValue(char* format,...)
    
    这里的format是一个 C 字符串,描述了要构建的 Python 对象。Py_BuildValue的以下参数是构建结果的 C 值。PyObject *结果是一个新的引用。
    下表列出了常用的代码字符串,其中零个或多个拼接成字符串格式。
    代码 C型 意义
    c char AC char 变成长度为 1 的 Python 字符串。
    d double AC double 成为 Python 浮点数。
    f float AC 浮点数变为 Python 浮点数。
    i int AC int 变成 Python int。
    l long AC long 成为 Python int。
    N PyObject* 传递一个 Python 对象并窃取一个引用。
    O PyObject* 传递一个 Python 对象并照常增加它。
    O& convert+void* 任意转换
    s char* C 以 0 结尾的 char* 到 Python 字符串,或 NULL 到 None。
    s# char*+int C char* 和长度到 Python 字符串,或 NULL 到 None。
    u Py_UNICODE* C 范围内,以 null 结尾的字符串到 Python Unicode,或 NULL 到 None。
    u# Py_UNICODE*+int C 范围的字符串和长度到 Python Unicode,或 NULL 到 None。
    w# char*+int 将单段缓冲区读/写到 C 地址和长度。
    z char* 与 s 一样,也接受 None(将 C char* 设置为 NULL)。
    z# char*+int 与 s# 一样,也接受 None(将 C char* 设置为 NULL)。
    (...) as per ... 从 C 值构建 Python 元组。
    [...] as per ... 从 C 值构建 Python 列表。
    {...} as per ... 从 C 值、交替键和值构建 Python 字典。
    代码 {...} 从偶数个 C 值构建字典,交替使用键和值。例如,Py_BuildValue("{issi}",23,"zig","zag",42) 返回像 Python 的 {23:'zig','zag':42} 这样的字典。