Python: C扩展初体验

孔秀
• 阅读 3681

前言

使用 Python 毋庸置疑减少了很多规则约束和开发成本,让我们能够更加专注于逻辑而非语法。但是得此失彼,开发效率提高了,却带来了运行性能的问题,所以就常常被其他门派追着暴打。
 
身为一个 pythoner,我们也很忧伤呀,怪我们咯..

万幸的是,虽然上帝关掉了我们一扇门,但是却为我们打开了另一扇窗,正因为底层是用 C语言 写的,所以我们可以将一些性能损耗比较大的功能,或者模块,通过 C语言 重写,然后 import xxxx 来无缝结合。

哪怕工作中比较少机会自己写C扩展, 了解这块的知识,也有利于我们更加深入了解 Python 的运行本质。

网上比较是通过 ctypes 或者 setup.py 的方式实现引用和编译安装,这边想试下最原始的方法~

快速开车

1. 实现接口函数

接口函数是什么意思?可以简单理解成就是 PythonC 的对接函数,举个栗子:

static PyObject *test(PyObject *self, PyObject *args){
    int arg1, arg2;
    if(!(PyArg_ParseTuple(args, "ii", &arg1, &arg2))){
        return NULL;
    }
    return Py_BuildValue("i",  arg1 + arg2 * 10);
}

我们可以看到这个函数和传统意义上的 C 用法用点不同了,特别是在函数形参那边的PyObject self, PyObject args

第一个参数是 PyObject *self,这个参数是Python内部使用的,可以不用管;

第二个参数是 PyObject *args,这个参数非常重要,因为这个揽括了所有传给函数的参数。它是一个参数列表,把所有的参数都整合到

一个 string, 因此,如果我们需要解析这些参数需要用特定的姿势!我们需要用到 PyArg_ParseTuple 来解开这个扣人心弦的入口!

PyArg_ParseTuple 函数说明:

  1. args就是需要转换的参数;
  2. ii 就是参数类型的格式符号,这里代表 int init;(更多类型请看官网:https://docs.python.org/2/c-a...
  3. 后面的 &arg1, &arg2 就是通过参数解析提取的值,存放的地方,这有点类似 C 的 scanf

很明显,这三个参数,在数量上存在这一定的联系,也就是,传进去两个 int参数,那么就肯定是对应了两个 ii,然后就会对应存在 两个实际的"容器"内,这里要注意,一不小心就会 Segmentation fault

对应有解析参数的,肯定也有 C模块 值转换成 Python对象 的,那就是 Py_BuildValue

Py_BuildValue 函数说明:

# 对比着来看
PyArg_ParseTuple(args, "ii", &arg1, &arg2)    Python -> C模块
Py_BuildValue("i",  arg1 + arg2 * 10);        C 模块 -> Python
  1. 第一个参数 和 PyArg_ParseTuple 的第二个参数一样,都是格式化符号;
  2. 第二个参数是需要转换的参数,函数 Py_BuildValue 会把所有的返回指都组装成 tuplePython

相关的官方文档:https://docs.python.org/2/c-a...

2. 定义方法列表

# 示例
static PyMethodDef  testMethods[] = {
    {"test", test, METH_VARARGS, "This is test"},
    {NULL, NULL, 0, NULL}
};

PyMethodDef 是一个 C结构体,用来完成一个映射,也就是便于方法查找,我们把需要被外面调用的方法都记录在这表内。

PyMethodDef 结构体成员说明:

  1. 第一个字段:在 Python 里面使用的方法名;
  2. 第二个字段:C 模块内的函数名;
  3. 第三个字段:方法参数类型,是无参数(METH_NOARGS) , 还是有位置参数(METH_VARARGS), 还是其他等等;
  4. 第四个字段:方法描述,就是通过 help() 或者 doc 可以看到的;

需要注意的是,这个列表的最后必须以 {NULL, NULL, 0, NULL} 的形式来代表声明结束,也有一些大佬用 {NULL, NULL},不过个人觉得写完整也不会累到哪去, 相反会比较直观。

# PyMethodDef 结构体定义源码, 取自 Python2.7 Include/methodobject.h
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 */
};

正因为存在这样的一份记录表,Python 才能够寻找到相应的函数

同样的,如果我们想要找一个模块的 Python 函数 对应什么的 C模块方法,也能通过这地方比较粗暴得知,例如 Python 的 list

# 取自 Python2.7 object/listobject.c
static PyMethodDef list_methods[] = {
    {"__getitem__", (PyCFunction)list_subscript, METH_O|METH_COEXIST, getitem_doc},
    {"__reversed__",(PyCFunction)list_reversed, METH_NOARGS, reversed_doc},
    {"__sizeof__",  (PyCFunction)list_sizeof, METH_NOARGS, sizeof_doc},
    {"append",          (PyCFunction)listappend,  METH_O, append_doc},
    {"insert",          (PyCFunction)listinsert,  METH_VARARGS, insert_doc},
    {"extend",      (PyCFunction)listextend,  METH_O, extend_doc},
    {"pop",             (PyCFunction)listpop,     METH_VARARGS, pop_doc},
    {"remove",          (PyCFunction)listremove,  METH_O, remove_doc},
    {"index",           (PyCFunction)listindex,   METH_VARARGS, index_doc},
    {"count",           (PyCFunction)listcount,   METH_O, count_doc},
    {"reverse",         (PyCFunction)listreverse, METH_NOARGS, reverse_doc},
    {"sort",            (PyCFunction)listsort,    METH_VARARGS | METH_KEYWORDS, sort_doc},
    {NULL,              NULL}           /* sentinel */
};

3. 实现初始化函数 (关键)

PyMODINIT_FUNC inittest(){
    Py_InitModule("test", testMethods);
}

需要特别注意的是,这个函数名不能像上面那样,这是有规定的,必须是 init + 模块名字,比方说,我的最后编译出来的文件是 test.so, 那我的函数名就是 inittest, 这样在 Python 导入 test 模块时,才能找到这个函数并调用。

这里调用了 Py_InitModule 函数来将模块名字和映射表结合在一起。表示 test 这个模块使用 testMethods 这个映射表。

4. 注意对象引用管理和内存泄露

Python 在对象管理、内存管理上面,有引用计数标记-清除分代回收等策略,其中引用计数发生的频率会非常非常高,依赖这个通常已经能够解决大部分的对象残留问题了。

所以,在我们编写 C扩展 时,也需要时刻谨记这步.

主要会用到下面两个宏:

1. 增加引用: Py_INCREF    例: Py_INCREF(pObj1)
2. 减少引用: Py_DECREF    例: Py_DECREF(pObj1)

不能直接使用 free/delete 释放,必须使用 Py_DECREF(pObj1), 然后 pObj1 = NULL 即可。

具体可以参考:
官网:https://docs.python.org/2/ext...
垃圾回收机制: http://www.wklken.me/posts/20...

编译导出

gcc  -I /usr/include/python2.7/ -fpic --shared -o test.so test.c

完整例子

test.c

#include<Python.h>
static PyObject *test(PyObject *self, PyObject *args){
    int arg1, arg2;
    if(!(PyArg_ParseTuple(args, "ii", &arg1, &arg2))){
        return NULL;
    }
    return Py_BuildValue("i",  arg1 + arg2 * 10);
}

static PyMethodDef  testMethods[] = {
    {"test", test, METH_VARARGS, "This is test"},
    {NULL, NULL}
};

PyMODINIT_FUNC inittest(){
    Py_InitModule("test", testMethods);
}

test.py

import test
print test.test(1, 2)   # 输出 21

欢迎各位大神指点交流, QQ讨论群: 258498217
转载请注明来源: https://segmentfault.com/a/11...

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
4年前
Python Challenge Level 18
初学Python,挑战一下流行的PythonChallenge,很不幸,卡在了18关~~被字符字节码之间的转换搞得焦头烂额,不过终于搞定了还是很happy的~~~主要的问题就是16进制形式的字符如何转成字节码(注意:不是encoding)如:\'89','50','4e','47','0d','0a','1a','0a','00
Stella981 Stella981
4年前
Scapy 从入门到放弃
0x00前言最近闲的没事,抽空了解下地表最强的嗅探和收发包的工具:scapy。scapy是一个python模块,使用简单,并且能灵活地构造各种数据包,是进行网络安全审计的好帮手。0x01安装因为2020年python官方便不再支持python2,所以使用python3安装。!(https://oscimg.oschina.net/os
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
孔秀
孔秀
Lv1
敬自己一杯酒,祝来年更顺。
文章
4
粉丝
0
获赞
0