[Python]C用API与Python交互

基础概念

实现

Python 的官方实现是 CPython. 另外还有其他实现, 如:

  • Pypy(使用 rPython 实现, 附有编译器, 有 JIT).
  • Jython
  • IronPython
  • Pyston

本文讨论 CPython.

虚拟机

与一些 做成对象的虚拟机 相比, CPython 的没有做成对象, 而是做成全局变量. 这是 Python 所谓 全局锁导致无法多线程运行 的缘由.
这是 CPython 初始化:

Py_Initialize();

这是 Lua 的初始化:

lua_State* L = lua_newstate(luaalloc, this);

根基

所有变量的读取, 设置都基于一个模块之上, 通常这个模块叫 __main__. 定义一个全局变量, 一个函数等, 实际上这些对象都"挂"在了这个 __main__ 模块之上.
如果需要设置一个变量如 a = 233, 实际上就是给 __main__ 模块设置了属性 __main__.a = 233.

对象

Python 中一切实体都是对象, 并且有一些统一的操作接口, 如一切对象都可以取 __doc__ 属性, 一切对象都有其所属类型, 这个类型也是对象.
一切对象原则上都是不可修改的, 除了 listdict.
一个对象有了 __dict__ 属性后, 即可以随意挂其他对象上去. 表面看起来像修改了对象, 实际上只是给对象的 __dict__ 属性下的字典加了一个条目. 如 __main__.a = 233 实际上是 __main__.__dict__['a'] = 233.
自定义的类的实例一般其 __dict__ 属性下都自带有字典. 有些类则没有 __dict__ 属性, 如 int, float 等.

引用计数

参考资料: 扩展Python模块系列(四)----引用计数问题的处理

查看 PyObject 的结构体, 可以发现有个 ob_refcnt 成员. 这是计算引用数的变量. 使用 API 获取对象时, 有些函数会修改变量的引用, 有些不会. 丢弃变量时注意确定好是否要调用 Py_DECREF 将引用数减一.
下面是操作引用数的宏:

  • Py_INCREF, 不可传入NULL
  • Py_XINCREF, 可传入NULL
  • Py_DECREF, 不可传入NULL
  • Py_XDECREF, 可传入NULL

释放对象, 并清零指针可以用 Py_CLEAR(可传入NULL).
获取引用数可以用 Py_REFCNT(不可传入NULL)

从 API 可以获取 PyObject 和传递 PyObject
获取对象时有两种结果

  • 新引用 (New Reference), API 内部对对象调用了 Py_INCREF 之后才传出来的对象, C 方面需要承担管理对象的职责, 并在不使用对象时, 调用 Py_DECREF 或将管理权转移.
  • 借引用 (Borrowed Reference), API 内部只是复制了对象地址出来, 除非 C 方面调用 Py_INCREF, 否则时刻有对象被回收的风险.

传递对象时有两种结果

  • 新引用 (默认), API 接收到对象后会对其调用 Py_INCREF, 之后会在 API 内部其他地方调用 Py_DECREF 如此自行管理对象, C 方面还需继续承担管理对象的职责.
  • 偷引用 (“steals” a reference), API 接收对象时不调用 Py_INCREF, 但依旧会在内部其他地方调用 Py_DECREF, C 方面可以转移管理权给 API, 之后 C 方面不能调用 Py_DECREF.

注意, 用 新引用 得来的对象不能直接传给 新引用 接口, C 方面还需调用 Py_DECREF 或转移管理权, 否则会造成内存泄漏.
借引用 得来的对象也不能直接传给 偷引用 接口.

Qt

Python.h 在 Qt 中编译时会报错. 原因是 Python.h 第448行, PyType_Spec 结构体使用了 slots 作为成员名. 解决方法是在该行前 #undef slots, 该行后重新 #define slots Q_SLOTS

初始化

全局虚拟机

void Py_Initialize(); // 初始化
int Py_IsInitialized(); // 是否初始化成功

void Py_Finalize(); // 释放

变量

官方文档见: 具体的对象层

据文档, 主要有六大类对象, 分别为:

  • 基本对象, 包括 Type, None.
  • 数值对象, 包括 int, bool, float, complex.
  • 序列对象, 包括 bytes, 字节数组, str, tuple, list, 结构体序列
  • 容器对象, 包括 dict, set
  • 函数对象, 可调用有函数, 方法, 实例方法, 不可调用有 Cell, 代码.
  • 其他对象

构建对象可以用 Python 代码构建, 也可以在 C 用 API 构建. 下面介绍用 API 构建的方法.

通用构建

其中前四大类大部分对象可以用 PyObject* Py_BuildValue(const char* format, ...); 完成构建, 返回 新引用. 其构造思路类似于 printf, 参数 format 使用类似 Python 语法的字符串, 其中一些值用指示字符代替, 在变参部分传入 C 的数据, 按顺序替换掉 format 中的指示字符, 以构造 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("ss", "hello", "world")                 ('hello', 'world')
Py_BuildValue("s#", "hello", 4)                       '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))

指示字符及其应传入的 C 的数据类型的转换表如下:

数字
指示字符Pyhton对象C备注
Bintunsigned char
bintchar
Hintunsigned short
hintshort
Iintunsigned int
iintint
kintunsigned long
lintlong
Kintunsigned long long
Lintlong long
nintPy_ssize_tssize_t
ffloatfloat
dfloatdouble
字符串
指示字符Pyhton对象C备注
cbyteschar长度为1
Cstrint长度为1
ybytes 或 Noneconst char*
y#bytes 或 Noneconst char*, int
s z Ustr 或 Noneconst char*utf-8
s# z# U#str 或 Noneconst char*, intutf-8
ustr 或 Noneconst wchar_t*UTF-16 or UCS-4
u#str 或 Noneconst wchar_t*, intUTF-16 or UCS-4
对象引用
指示字符Pyhton对象C备注
O S UobjectPyObject*对象引用计数加一, 新引用
NobjectPyObject*对象引用计数不变, 偷引用
O&objectconverter, anything调用converter(anything)创建对象
容器
指示字符Pyhton对象C备注
(…)tuple用多个对象创建元组
[…]list用多个对象创建列表
{…: …, …}dict用多对对象创建字典

具体构建

官方文档见: 具体的对象层

C函数

给 Python 调用的 C 函数有形参要求. 通常会作为 C API 的包装(wrapper)函数, 也可以直接用这些函数写业务逻辑.

C 函数有两种方法提供给 Python. 一是作为自定义的模块的函数加载入 Python. 二是用 PyCFunction_New 生成独立的C函数对象.

官方文档: 使用 C 或 C++ 扩展 Python, Common Object Structures PyMethodDef

这里介绍用 PyCFunction_New 生成独立的C函数对象(新引用).

  1. 函数实现

函数类型依调用约定的不同, 有如下几种:

PyObject *(*PyCFunction)(PyObject *self, PyObject *args);
PyObject *(*_PyCFunctionFast) (PyObject *self, PyObject **args, Py_ssize_t nargs, PyObject *kwnames);
PyObject *(*PyCFunctionWithKeywords)(PyObject *self, PyObject *args, PyObject *kwargs);
PyObject *(*PyNoArgsFunction)(PyObject *self);

可以选择任意一种进行实现. 参数传来的对象是 借引用, 无需调用 Py_DECREF.
一个使用 METH_VARARGS 调用约定的做加法的函数的例子:

PyObject* myaddfunc(PyObject* self, PyObject* args) {
    int ok;                                   
    int a = -1, b = -1;                       
    ok = PyArg_ParseTuple(args, "ii", &a, &b);
    if(ok) {                                  
        return PyLong_FromLong(a + b);        
    } else {                                  
        printf("失败\n");                       
    }                                         
    Py_RETURN_NONE;                           
}
  1. 填充 函数描述结构体
PyMethodDef myaddmethoddef = {
	"add", // 内部方法名
	(PyCFunction*)myaddfunc, // 函数地址, 但无论函数原型如何, 都要转为PyCFunction*型, 但函数真正原型要遵守调用约定的规则
	METH_VARARGS, // 调用约定
	"add function." // __doc__
};

其中调用约定有如下位可选(相互间即可用 | 组合):

  • METH_VARARGS, 要求函数形参为 PyCFunction, 传给函数的第一个参数是模块对象(模块函数)或对象实例(对象方法), 下同. 第二个是 tuple 类型的实参元组. 通常对应的 py 函数形参如: func(*args).
  • METH_KEYWORDS, 要求函数形参为 PyCFunctionWithKeywords, 第二个参数同上, 第三个是 dict 类型的实参字典. 通常对应的 py 函数形参如: func(*args, **kwargv).
  • METH_NOARGS, 要求函数形参为 PyCFunctionPyNoArgsFunction. 使用前者时, 第二个参数值为 NULL.
  • METH_O, 要求函数形参为 PyCFunction. 该位假定函数只有一个形参, 第二个参数直接即为传入的对象. 已知参数只有一个时, 相对于使用 METH_VARARGS, 使用 METH_O 可以避免对传入的 tuple 解析以提高效率.
  • METH_CLASS, 为类方法使用.
  • METH_STATIC, 为类静态方法使用.
  • METH_COEXIST.

详见文档.

  1. 创建 Python 对象
PyObject* myaddmethodobj = PyCFunction_New(&myaddmethoddef, NULL);

到此创建结束.
但是关于函数至少还有以下内容, 笔者还未解决: 闭包, 异常的抛出.
目前 PyCFunction_New 没有官方文档, 这里 有对该函数使用姿势的讨论.

交互

从 C 设值到 Python

设值 指的是这样的操作:

obj1.name = obj2

从 C 设值到 Python, 核心是调用 PyObject_SetAttrString. (两个对象都是 新引用)

PyObject* obj1;
PyObject* obj2; // 已预先将对象取到 C 了
int ok = PyObject_SetAttrString(obj1, "name", obj2);

而 设全局变量 指的是这样的操作:

name = obj2

实际上相当于

__main__.name = obj2
PyObject* main_mod = PyImport_ImportModule("__main__");
// 导入模块 __main__, 不过这个模块本身就存在(在 sys.modules), 所以不会真的导入, 而直接返回模块对象, 但是引用计数会加一
PyObject* obj2;
int ok = PyObject_SetAttrString(main_mod, "name", obj2); // main_mod 和 obj2 都是新引用传递
Py_DECREF(main_mod); // 减引用, 释放对象

从 Python 取值到 C

取值, 指的是这样的操作

obj2 = obj1.name

从 Python 取值到 C, 核心是调用 PyObject_GetAttrString. 返回 新引用.

PyObject* obj1;
PyObject* obj2 = PyObject_GetAttrString(obj1, "name");
Py_DECREF(obj2);

从全局取值亦类同上节.

PyObject* main_mod = PyImport_ImportModule("__main__");
PyObject* obj2 = PyObject_GetAttrString(main_mod, "name");
Py_DECREF(obj2);
Py_DECREF(main_mod);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值