官方的 Python/C API 是针对 CPython 的实现的:公开了许多内部细节,使得 API 实现难度较大;而且,如果要为 PyPy、GraalPython、Jython、IronPython 等替代实现开发 API,更是各类问题多多。最近发现了一个性能更好的开源 Python 扩展,HPy。简单试用后,感觉值得推荐。
HPy 介绍
HPy 提供了一个新的 API,以用 C 扩展 Python,有零开销、更快速、方便调试、通用的二进制文件(不用任何修改,可在 CPython、PyPy、GraalPython 等解释器上直接加载),以及更加符合时代标准等优点。换句话说,如果您做过 C 中嵌入 Python 的开发,必定熟悉这行代码:
代码语言:javascript复制#include <Python.h>
现在,如果你使用 HPy,则可以替换为:
代码语言:javascript复制
#include <hpy.h>
HPy 优点
但是,虽然直接介绍了代码,但为什么要用 HPy?作为一个 2019 年才诞生的项目,HPy 设计挺优秀,官方言其具有如下优点:
- CPython 之上的零开销:用 HPy 编写的扩展相比于官方 CPython,未有任何额外开销。
- 在 PyPy、GraalPython(目前比较热火的 Python3 实现,性能提升极大,但对 C 扩展) 等 CPython 替代实现上,运行更快。
- 更友好的调试模式:HPy 调试模式可以自动检测诸多错误。在 HPy 的调试模式下,您可以很容易地识别常见问题,如内存泄漏、对象的无效生存期、API 的无效使用等。
- 通用的二进制文件:为 HPy 通用 ABI 构建的扩展,可以在 CPython、PyPy、GraalPython 之上不用任何修改,直接加载运行。
- 更好的 API:标准的 Python/C API 具有其产生时代的特性限制,而 HPy 的设计可以克服一些限制。让扩展 API 更加一致,更易写易读,并且可使 bug 更易于暴露。
HPy 类库
目前,已经有 ultrajson-hpy(ultrajson 是 Python 生态中极速的 JSON 编解码器)、piconump(类似于 ndarray),以及 numpy-hpy(大名鼎鼎的 numpy)等模块可与 HPy 兼容试用。可以看到,还处于开发初期,但科学计算、数据解析已经可以尝试了。
HPy 示例
概念和优点,都是官方的说法,我们实际代码操练一下,看看试用体验,以及 HPy 是否具有优势。示例很简单,即实现一个 add
运算的扩展。
首先,我们用标准 Python/C 扩展方式实现
命名文件为 hello_old.c
:
#include <Python.h>
static PyObject* add(PyObject* self, PyObject* args) {
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b))
return NULL;
return PyLong_FromLong(a b);
}
static PyMethodDef HelloMethods[] = {
{"add", (PyCFunction)add, METH_VARARGS, "两个整数相加"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"hello_old",
"使用标准 Python/C API",
-1,
HelloMethods,
};
PyMODINIT_FUNC
PyInit_hello_old(void) {
return PyModule_Create(&moduledef);
}
我们通过 setup.py
编译扩展文件:
from setuptools import setup, Extension
setup(
name="hello",
ext_modules = [
Extension('hello_old', ['hello_old.c']),
],
)
最后,我们运行 setup.py
并使用此扩展实现:
$ python setup.py build_ext --inplace
...
$ python
>>> import hello_old
>>> hello_old.add(10, 20)
30
然后,我们用 HPy 方式实现
命名文件为 hello_new.c
:
#include <hpy.h>
HPyDef_METH(add, "add", add_impl, HPyFunc_VARARGS,
.doc = "两个整数相加");
static HPy add_impl(HPyContext ctx, HPy self, HPy *args, HPy_ssize_t nargs) {
long a, b;
if (!HPyArg_Parse(ctx, NULL, args, nargs, "ll", &a, &b))
return HPy_NULL;
return HPyLong_FromLong(ctx, a b);
}
static HPyDef *hello_defines[] = {
&add,
NULL
};
static HPyModuleDef moduledef = {
HPyModuleDef_HEAD_INIT,
.m_name = "hello_new",
.m_doc = "使用 HPy API",
.m_size = -1,
.defines = hello_defines,
};
HPy_MODINIT(hello_new)
static HPy init_hello_new_impl(HPyContext ctx) {
return HPyModule_Create(ctx, &moduledef);
}
同样,我们需要通过 setup.py
编译扩展文件。我们在 setup.py
加入 hpy_ext_modules
定义项,一次将两种方式实现的扩展全部编译:
from setuptools import setup, Extension
setup(
name="hello",
ext_modules = [
Extension('hello_old', ['hello_old.c']),
],
hpy_ext_modules = [
Extension('hello_new', ['hello_new.c']),
],
setup_requires=['hpy.devel'],
)
HPy 扩展实现方式的编译,需要将参数 --hpy-abi=universal
传递给 setup.py
:
$ python setup.py --hpy-abi=universal build_ext --inplace
$ ls -1 *.so
hello_new.hpy.so
hello_old.cpython-38-x86_64-linux-gnu.so
我们可以看到,是兼容 32位和 64 位处理器的。好,我们看看能不能跑的通:
代码语言:javascript复制
$ python
>>> import hello_old, hello_new
>>> hello_old.add(10, 20)
30
>>> hello_new.add(30, 40)
70
>>> hello_new
<module 'hello_new' from '/.../hello-hpy/hello_new.py'>
>>> hello_new.__file__
'/.../hello-hpy/hello_new.hpy.so'
简单试用就是以上所述,总体说来体验还行。但是上面提到,HPy 还处于开发初期,建议使用 github 仓库版本。
本次体验到此结束,以后在使用过程中,如果有新的推荐点,笔者再分享。
谢谢您的阅读!