使用C语言编写Python模块-引子【转】

python

为什么要用C语言写Python模块,是Python不够香么?还是觉得头发还茂盛?都不是。因为C语言模块有几个显而易见的好处:

  1. 可以使用Python调用C标准库、系统调用等;
  2. 假设已经有了一堆C代码实现的功能,可以不用重写,岂不美滋滋;
  3. 性能?也算;
  4. 其他一些好处。

注:以下代码基于Python3。

开局举个栗

In a nutshell,用C编写Python模块就是下面几步:

准备工作

#include<Python.h>

// 没错,这就够了,什么stdio.h就都有了

定义API

static PyObject* say_hello(PyObject* self, PyObject* args) {

printf("Hello world, I just a demo.");

}

注册API

// PyMethodDef 是一个结构体

static PyMethodDef my_methods[] = {

{ "say", say_hello, 0, "Just show a greeting." },

{NULL, NULL, 0, NULL}

};

注册模块

static struct PyModuleDef my_module = {

PyModuleDef_HEAD_INIT,

"dummy",

NULL,

-1,

my_methods

};

初始化

PyMODINIT_FUNC PyInit_mymodule(void) {

return PyModule_Create(&my_module);

}

编译

编译也可以手动编译,只不过,懒。。。

python">from distutils.core import setup, Extension

module1 = Extension(\'dummy\',

define_macros = [(\'MAJOR_VERSION\', \'1\'),

(\'MINOR_VERSION\', \'0\')],

sources = [\'my_module.c\'])

setup (name = \'DummyModule\',

version = \'1.0\',

description = \'This is a demo package\',

author = \'zmyzhou\',

author_email = \'no@email.here.com\',

url = \'https://docs.python.org/extending/building\',

long_description = \'\'\'This is really just a demo package.\'\'\',

ext_modules = [module1]

)

运行

export PYTHONPATH=/home/example

(misc) $ python

Python 3.5.2 (default, Oct 8 2019, 13:06:37)

[GCC 5.4.0 20160609] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> import dummy

>>> dummy.say()

Hello world, I just a demo.

>>>

解剖麻雀啦

总得来说,想用C写Python扩展模块步骤基本就是上面提到的这几个步骤就可以完成(重复啰嗦):

  1. 定义你需要暴露给CPython解析器的函数;
  2. 用一个PyMethodDef结构体列表去给出所有需要暴露的函数的元数据,对第一步中所定义的函数进行映射以及说明,让解析器知道文怎去构造一个Python调用;
  3. 用一个PyModuleDef去给出此模块的元数据;
  4. 给出一个当Python解释器加载该模块时候的构造函数PyInit_<Module_name>, 其中Module_name表示该模块的名字,也就是在PyModuleDef中给出的模块名,例子中是dummy,那么这个函数名最后就是PyInit_dummy

虽然说简洁是智慧的精华,但是也太简单了,裤子都脱了,你就给我看这个?

少侠且慢动手,容我解释解释。

API 需要符合什么要求?

由于在Python语言中,在几乎所有场景中对类型时不加以区分的,而C语言是区分类型的,那怎么办?解决办法是只用一种C类型表示,而这个类型就是PyObject。而这个PyObject到底是什么可以暂且不管,就好似总说五百年前是一家,究竟五百年前这家户主是谁,我们很多时候没必要知道。

此外,由于几乎多有Python对象对生存在堆上,因此我们接口中的对象(变量)也应该生存在堆上,所以我们用指针来索引,即PyObject*。到此,我们的函数原型呼之欲出。

在Python中我们定义一个函数时这样子:

def func(*args):

# do something here

那么我们C中定义的函数也类似:

PyObject* func(PyObject* self, PyObject* args) {

// I too do something here

}

是不是似曾相识?如果这个函数是个模块函数,那么self表示NULL或者一个特定指向的指针,如果是类中的方法,self就表示为当前调用该方法的实例;args就表示参数列表。比如,我们觉得上面例子中``say_hello`总是复读机式输出同一句话太单调,我们现在想让他鹦鹉学舌,我们可以改成:

PyObject* echo(PyObject* self, PyObject* args) {

const char* what;

PyArg_ParseTuple(args, "s", &what);

printf("Python said: %s", what);

return Py_None;

}

输出为:

>>> import dummy

>>> dummy.echo(\'Hello there!\')

Python said: Hello there!

>>>

上面echo的例子中我们发现了一个奇怪的东西混了进来:PyArg_ParseTuple。这是什么?我说是魔法肯定被打。

输入参数和返回处理

输入

上面说过,Python中我们很少关心某个变量是什么类型,我们用PyObject表示所有从Python传过来的值类型,但是由于C语言是强类型语言,只用一种类型是没办法正常工作的。因此我们需要把这种类型变成C语言中相应的类型。就好似古代夜观天象,每天都可以出现流星,但是一般人也看不懂天象啊,这只能让星官来解释,星官根据不同现象来解释,是大吉大利还是不详。PyArg_ParseTuple就是做这个翻译的工作,其函数声明如下:

int PyArg_ParseTuple(PyObject *args, const char* format, ...);

其中args就是API中的args参数,format就是你要将args中的对应参数翻译成C语言中的什么类型。例如上面echo的例子中,我们就将其翻译成了char*字符串。通过format="s"来指示PyArg_ParseTuple我们传入的args第一个参数是字符串。如果我们还想多几个参数,那么怎么办?好办。我们使用format="si"来表示我们第一个参数是字符串,第二个参数是整型。

PyObject* echo(PyObject* self, PyObject* args) {

const char* what;

int count;

PyArg_ParseTuple(args, "si", &what, &count);

int i = 0;

for(; i < count; i++)

printf("Python said: %s \n", what);

return Py_None;

}

这样我们的输出就变成了:

>>> import dummy

>>> dummy.echo(\'repeat my word 3 times.\', 3)

Python said: repeat my word 3 times.

Python said: repeat my word 3 times.

Python said: repeat my word 3 times.

>>>

更多关于如何解析Python穿过来的参数的方法以及如何使用相对应的format,请参阅这里。

返回

来而不往非礼也。有传进来的,那就肯定有传出去的。事情完成没完成都应该对请求的人有个交代。那我们怎么把特定的C类型变量丢还给Python呢?使用Py_BuildValue,其实就是类似于PyArg_ParseTuple反过来。我们例子中返回来Python中的None,我们也可以返回一句话。例如:

PyObject* echo(PyObject* self, PyObject* args) {

const char* what;

int count;

char* feedback = "Job is done.";

PyArg_ParseTuple(args, "si", &what, &count);

int i = 0;

for(; i < count; i++)

printf("Python said: %s \n", what);

return Py_BuildValue("s", feedback);

}

>>> fb = dummy.echo(\'Repeat my word 4 time and give me feedback.\', 4)

Python said: Repeat my word 4 time and give me feedback.

Python said: Repeat my word 4 time and give me feedback.

Python said: Repeat my word 4 time and give me feedback.

Python said: Repeat my word 4 time and give me feedback.

>>> print(fb)

Job is done.

>>>

更多细节请参阅这里

怎么注册API?

注册API,需要用到一个PyMethodDef结构体,其定义如下:

struct PyMethodDef {

const char *ml_name; /* The name of the built-in function/method */

PyCFunction ml_meth; /* The C function that implements it */

int ml_flags; /* Combination of METH_xxx flags, which mostly

describe the args expected by the C func */

const char *ml_doc; /* The __doc__ attribute, or NULL */

};

typedef struct PyMethodDef PyMethodDef

这里主要注意的是ml_flags,它控制着Python怎样把参数传过来,我上面例子中用到的一直是METH_VARARGS这也是一种比较常用的标志,它表示我们所注册的API接收两个参数,一个self用于表示调用者本身,另一个args表示个tuple。还有其他几种标志可选。另外注意区分ml_nameml_meth,前者表示在Python中调用时的名字,后者表示在C语言中定义的方法名字。详情请看这里。

怎么注册模块?

与注册API类似,注册模块也用到一个结构体PyModuleDef,其定义如下:

typedef struct PyModuleDef{

PyModuleDef_Base m_base;

const char* m_name;

const char* m_doc;

Py_ssize_t m_size;

PyMethodDef *m_methods;

struct PyModuleDef_Slot* m_slots;

traverseproc m_traverse;

inquiry m_clear;

freefunc m_free;

}PyModuleDef;

怎么看着比我们例子中的多了很多项?其实多出来的我们只需要特别关心m_name, m_doc, m_size, m_methods这四项。第一项PyModuleDef_Base的值肯定是PyModuleDef_HEAD_INIT,这是个宏,具体是啥我们不需要管。

要注意的是,n_name就是将来你在Python中导入该模块时的名字,比如这里我们设置n_name="dummy",我们在使用的时候就是import dummym_doc就是我们使用dummy.__doc__将输出的内容,属于对模块的说明,例如:

static struct PyModuleDef my_module = {

PyModuleDef_HEAD_INIT,

"dummy",

"Sometimes NO DOC is the best DOC.",

-1,

my_methods

};

则输出为:

>>> import dummy

>>> print(dummy.__doc__)

Sometimes NO DOC is the best DOC.

m_methods就是上面注册的API。详情看这里。

The end? Not yet.

另外还有个很重要的概念就是引用计数,这个一时半会也说不清,这篇文章的目的本来就是抛砖引玉,大概了解用C语言开发Python模块是个什么流程,我们的目的也达到了。

很繁琐,我一个写Python、三行代码就可以为所欲为的人,怎么忍受得了这些花里胡哨?幸运的是,所有程序员的痛是一样的,大家都不喜欢繁琐,大家都追求的是简洁。因此诞生了Boost.python这种库,之后由于Boost太庞大,又出现了类似功能的轻量级pybind11。例如使用pybind11,下面代码个就可以完成我们上面繁琐的工作:

#include<pybind11/pybind11.h>

namespace py = pybind11;

char* greet() {

return "Hello, World!";

}

PYBIND11_MODULE(example, m) {

m.doc() = "pybind11 example module";

// Add bindings here

m.def("say", greet);

}

然后用一下命令编译并设置PYTHONPATH:

c++ -O3 -Wall -shared -std=c++11 -I/home/example/playground/pybind11/include my_module.c -o example.so -I/usr/include/python3.5m -I//home/example/playground/pybind11/include -fPIC

export PYTHONPATH=/home/example

Python中执行:

>>> import example

>>> example.say()

\'Hello, World!\'

>>>

瞬间感觉头发保住了。

等等,不是说用C吗?为什么最后乱入C++11?都差不多,who cares?

References

https://docs.python.org/3.7/extending/extending.html#the-module-s-method-table-and-initialization-function

https://docs.python.org/3/c-api/index.html

https://www.python.org/dev/peps/pep-0007/

https://github.com/pybind/pybind11

 



 

这就是我的底线!!欢迎搜索关注TensorBoy , 学习使我快乐!



作者:SunnyZhou1024
链接:https://www.jianshu.com/p/47590edc355c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以上是 使用C语言编写Python模块-引子【转】 的全部内容, 来源链接: utcz.com/z/386519.html

回到顶部